mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-24 11:52:40 +02:00
feat: cross-script transliteration matching for Tidal/Qobuz and skip-downloaded option for CSV import
This commit is contained in:
@@ -0,0 +1,59 @@
|
|||||||
|
package gobackend
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestQobuzTitlesMatchCrossScript(t *testing.T) {
|
||||||
|
t.Run("rejects unrelated cross-script titles", func(t *testing.T) {
|
||||||
|
if qobuzTitlesMatch("パンツ脱げるもん!", "Warrior of the Darkness") {
|
||||||
|
t.Fatalf("expected unrelated cross-script titles to not match")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("accepts transliterated japanese title", func(t *testing.T) {
|
||||||
|
if !qobuzTitlesMatch("パンツ脱げるもん!", "Pantsu Nugeru Mon") {
|
||||||
|
t.Fatalf("expected transliterated japanese title to match")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQobuzArtistsMatchCrossScript(t *testing.T) {
|
||||||
|
t.Run("rejects unrelated cross-script artists", func(t *testing.T) {
|
||||||
|
if qobuzArtistsMatch("TakeponG", "陳奕迅") {
|
||||||
|
t.Fatalf("expected unrelated cross-script artists to not match")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("accepts transliterated japanese artist", func(t *testing.T) {
|
||||||
|
if !qobuzArtistsMatch("たけぽんぐ", "takepong") {
|
||||||
|
t.Fatalf("expected transliterated japanese artist to match")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTidalTitlesMatchCrossScript(t *testing.T) {
|
||||||
|
t.Run("rejects unrelated cross-script titles", func(t *testing.T) {
|
||||||
|
if titlesMatch("パンツ脱げるもん!", "Warrior of the Darkness") {
|
||||||
|
t.Fatalf("expected unrelated cross-script titles to not match")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("accepts transliterated japanese title", func(t *testing.T) {
|
||||||
|
if !titlesMatch("パンツ脱げるもん!", "Pantsu Nugeru Mon") {
|
||||||
|
t.Fatalf("expected transliterated japanese title to match")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTidalArtistsMatchCrossScript(t *testing.T) {
|
||||||
|
t.Run("rejects unrelated cross-script artists", func(t *testing.T) {
|
||||||
|
if artistsMatch("TakeponG", "陳奕迅") {
|
||||||
|
t.Fatalf("expected unrelated cross-script artists to not match")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("accepts transliterated japanese artist", func(t *testing.T) {
|
||||||
|
if !artistsMatch("たけぽんぐ", "takepong") {
|
||||||
|
t.Fatalf("expected transliterated japanese artist to match")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
+168
-10
@@ -81,13 +81,160 @@ func qobuzArtistsMatch(expectedArtist, foundArtist string) bool {
|
|||||||
expectedLatin := qobuzIsLatinScript(expectedArtist)
|
expectedLatin := qobuzIsLatinScript(expectedArtist)
|
||||||
foundLatin := qobuzIsLatinScript(foundArtist)
|
foundLatin := qobuzIsLatinScript(foundArtist)
|
||||||
if expectedLatin != foundLatin {
|
if expectedLatin != foundLatin {
|
||||||
GoLog("[Qobuz] Artist names in different scripts, assuming match: '%s' vs '%s'\n", expectedArtist, foundArtist)
|
if qobuzCrossScriptEquivalent(expectedArtist, foundArtist) {
|
||||||
return true
|
GoLog("[Qobuz] Artist names in different scripts but transliteration matched: '%s' vs '%s'\n", expectedArtist, foundArtist)
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func qobuzNormalizeScriptAware(value string) string {
|
||||||
|
normalized := strings.ToLower(strings.TrimSpace(value))
|
||||||
|
normalized = CleanToASCII(JapaneseToRomaji(normalized))
|
||||||
|
normalized = strings.Join(strings.Fields(normalized), " ")
|
||||||
|
return strings.TrimSpace(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
func qobuzCrossScriptEquivalent(expected, found string) bool {
|
||||||
|
normExpected := qobuzNormalizeScriptAware(expected)
|
||||||
|
normFound := qobuzNormalizeScriptAware(found)
|
||||||
|
|
||||||
|
if normExpected == "" || normFound == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if normExpected == normFound {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
compactExpected := strings.ReplaceAll(normExpected, " ", "")
|
||||||
|
compactFound := strings.ReplaceAll(normFound, " ", "")
|
||||||
|
if len(compactExpected) >= 6 && len(compactFound) >= 6 {
|
||||||
|
if compactExpected == compactFound ||
|
||||||
|
strings.Contains(compactExpected, compactFound) ||
|
||||||
|
strings.Contains(compactFound, compactExpected) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
shorterLen := len(compactExpected)
|
||||||
|
if len(compactFound) < shorterLen {
|
||||||
|
shorterLen = len(compactFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
maxDistance := 1
|
||||||
|
if shorterLen >= 10 {
|
||||||
|
maxDistance = 2
|
||||||
|
}
|
||||||
|
if shorterLen >= 16 {
|
||||||
|
maxDistance = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
if qobuzEditDistanceWithin(compactExpected, compactFound, maxDistance) {
|
||||||
|
if qobuzCommonPrefixLen(compactExpected, compactFound) >= 4 ||
|
||||||
|
qobuzCommonSuffixLen(compactExpected, compactFound) >= 4 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func qobuzCommonPrefixLen(a, b string) int {
|
||||||
|
max := len(a)
|
||||||
|
if len(b) < max {
|
||||||
|
max = len(b)
|
||||||
|
}
|
||||||
|
count := 0
|
||||||
|
for i := 0; i < max; i++ {
|
||||||
|
if a[i] != b[i] {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
func qobuzCommonSuffixLen(a, b string) int {
|
||||||
|
i := len(a) - 1
|
||||||
|
j := len(b) - 1
|
||||||
|
count := 0
|
||||||
|
for i >= 0 && j >= 0 {
|
||||||
|
if a[i] != b[j] {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
i--
|
||||||
|
j--
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
func qobuzEditDistanceWithin(a, b string, maxDistance int) bool {
|
||||||
|
if maxDistance < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if a == b {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
lenA := len(a)
|
||||||
|
lenB := len(b)
|
||||||
|
diff := lenA - lenB
|
||||||
|
if diff < 0 {
|
||||||
|
diff = -diff
|
||||||
|
}
|
||||||
|
if diff > maxDistance {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
prev := make([]int, lenB+1)
|
||||||
|
for j := 0; j <= lenB; j++ {
|
||||||
|
prev[j] = j
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 1; i <= lenA; i++ {
|
||||||
|
curr := make([]int, lenB+1)
|
||||||
|
curr[0] = i
|
||||||
|
minInRow := curr[0]
|
||||||
|
|
||||||
|
for j := 1; j <= lenB; j++ {
|
||||||
|
cost := 0
|
||||||
|
if a[i-1] != b[j-1] {
|
||||||
|
cost = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
insertCost := curr[j-1] + 1
|
||||||
|
deleteCost := prev[j] + 1
|
||||||
|
replaceCost := prev[j-1] + cost
|
||||||
|
|
||||||
|
best := insertCost
|
||||||
|
if deleteCost < best {
|
||||||
|
best = deleteCost
|
||||||
|
}
|
||||||
|
if replaceCost < best {
|
||||||
|
best = replaceCost
|
||||||
|
}
|
||||||
|
|
||||||
|
curr[j] = best
|
||||||
|
if best < minInRow {
|
||||||
|
minInRow = best
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if minInRow > maxDistance {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
prev = curr
|
||||||
|
}
|
||||||
|
|
||||||
|
return prev[lenB] <= maxDistance
|
||||||
|
}
|
||||||
|
|
||||||
func qobuzSplitArtists(artists string) []string {
|
func qobuzSplitArtists(artists string) []string {
|
||||||
normalized := artists
|
normalized := artists
|
||||||
normalized = strings.ReplaceAll(normalized, " feat. ", "|")
|
normalized = strings.ReplaceAll(normalized, " feat. ", "|")
|
||||||
@@ -177,8 +324,10 @@ func qobuzTitlesMatch(expectedTitle, foundTitle string) bool {
|
|||||||
expectedLatin := qobuzIsLatinScript(expectedTitle)
|
expectedLatin := qobuzIsLatinScript(expectedTitle)
|
||||||
foundLatin := qobuzIsLatinScript(foundTitle)
|
foundLatin := qobuzIsLatinScript(foundTitle)
|
||||||
if expectedLatin != foundLatin {
|
if expectedLatin != foundLatin {
|
||||||
GoLog("[Qobuz] Titles in different scripts, assuming match: '%s' vs '%s'\n", expectedTitle, foundTitle)
|
if qobuzCrossScriptEquivalent(expectedTitle, foundTitle) {
|
||||||
return true
|
GoLog("[Qobuz] Titles in different scripts but transliteration matched: '%s' vs '%s'\n", expectedTitle, foundTitle)
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -702,8 +851,11 @@ func (q *QobuzDownloader) SearchTrackByMetadataWithDuration(trackName, artistNam
|
|||||||
GoLog("[Qobuz] Title matches: %d out of %d results\n", len(titleMatches), len(allTracks))
|
GoLog("[Qobuz] Title matches: %d out of %d results\n", len(titleMatches), len(allTracks))
|
||||||
|
|
||||||
tracksToCheck := titleMatches
|
tracksToCheck := titleMatches
|
||||||
if len(titleMatches) == 0 {
|
if strings.TrimSpace(trackName) != "" && len(titleMatches) == 0 {
|
||||||
GoLog("[Qobuz] WARNING: No title matches for '%s', checking all %d results\n", trackName, len(allTracks))
|
return nil, fmt.Errorf("no tracks found with matching title (expected '%s')", trackName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(trackName) == "" {
|
||||||
for i := range allTracks {
|
for i := range allTracks {
|
||||||
tracksToCheck = append(tracksToCheck, &allTracks[i])
|
tracksToCheck = append(tracksToCheck, &allTracks[i])
|
||||||
}
|
}
|
||||||
@@ -1152,10 +1304,16 @@ func downloadFromQobuz(req DownloadRequest) (QobuzDownloadResult, error) {
|
|||||||
if track == nil {
|
if track == nil {
|
||||||
GoLog("[Qobuz] Trying metadata search: '%s' by '%s'\n", req.TrackName, req.ArtistName)
|
GoLog("[Qobuz] Trying metadata search: '%s' by '%s'\n", req.TrackName, req.ArtistName)
|
||||||
track, err = downloader.SearchTrackByMetadataWithDuration(req.TrackName, req.ArtistName, expectedDurationSec)
|
track, err = downloader.SearchTrackByMetadataWithDuration(req.TrackName, req.ArtistName, expectedDurationSec)
|
||||||
if track != nil && !qobuzArtistsMatch(req.ArtistName, track.Performer.Name) {
|
if track != nil {
|
||||||
GoLog("[Qobuz] Artist mismatch from metadata search: expected '%s', got '%s'. Rejecting.\n",
|
if !qobuzTitlesMatch(req.TrackName, track.Title) {
|
||||||
req.ArtistName, track.Performer.Name)
|
GoLog("[Qobuz] Title mismatch from metadata search: expected '%s', got '%s'. Rejecting.\n",
|
||||||
track = nil
|
req.TrackName, track.Title)
|
||||||
|
track = nil
|
||||||
|
} else if !qobuzArtistsMatch(req.ArtistName, track.Performer.Name) {
|
||||||
|
GoLog("[Qobuz] Artist mismatch from metadata search: expected '%s', got '%s'. Rejecting.\n",
|
||||||
|
req.ArtistName, track.Performer.Name)
|
||||||
|
track = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+153
-4
@@ -1196,13 +1196,160 @@ func artistsMatch(spotifyArtist, tidalArtist string) bool {
|
|||||||
spotifyLatin := isLatinScript(spotifyArtist)
|
spotifyLatin := isLatinScript(spotifyArtist)
|
||||||
tidalLatin := isLatinScript(tidalArtist)
|
tidalLatin := isLatinScript(tidalArtist)
|
||||||
if spotifyLatin != tidalLatin {
|
if spotifyLatin != tidalLatin {
|
||||||
GoLog("[Tidal] Artist names in different scripts, assuming match: '%s' vs '%s'\n", spotifyArtist, tidalArtist)
|
if crossScriptEquivalent(spotifyArtist, tidalArtist) {
|
||||||
return true
|
GoLog("[Tidal] Artist names in different scripts but transliteration matched: '%s' vs '%s'\n", spotifyArtist, tidalArtist)
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeScriptAware(value string) string {
|
||||||
|
normalized := strings.ToLower(strings.TrimSpace(value))
|
||||||
|
normalized = CleanToASCII(JapaneseToRomaji(normalized))
|
||||||
|
normalized = strings.Join(strings.Fields(normalized), " ")
|
||||||
|
return strings.TrimSpace(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
func crossScriptEquivalent(expected, found string) bool {
|
||||||
|
normExpected := normalizeScriptAware(expected)
|
||||||
|
normFound := normalizeScriptAware(found)
|
||||||
|
|
||||||
|
if normExpected == "" || normFound == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if normExpected == normFound {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
compactExpected := strings.ReplaceAll(normExpected, " ", "")
|
||||||
|
compactFound := strings.ReplaceAll(normFound, " ", "")
|
||||||
|
if len(compactExpected) >= 6 && len(compactFound) >= 6 {
|
||||||
|
if compactExpected == compactFound ||
|
||||||
|
strings.Contains(compactExpected, compactFound) ||
|
||||||
|
strings.Contains(compactFound, compactExpected) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
shorterLen := len(compactExpected)
|
||||||
|
if len(compactFound) < shorterLen {
|
||||||
|
shorterLen = len(compactFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
maxDistance := 1
|
||||||
|
if shorterLen >= 10 {
|
||||||
|
maxDistance = 2
|
||||||
|
}
|
||||||
|
if shorterLen >= 16 {
|
||||||
|
maxDistance = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
if editDistanceWithin(compactExpected, compactFound, maxDistance) {
|
||||||
|
if commonPrefixLen(compactExpected, compactFound) >= 4 ||
|
||||||
|
commonSuffixLen(compactExpected, compactFound) >= 4 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func commonPrefixLen(a, b string) int {
|
||||||
|
max := len(a)
|
||||||
|
if len(b) < max {
|
||||||
|
max = len(b)
|
||||||
|
}
|
||||||
|
count := 0
|
||||||
|
for i := 0; i < max; i++ {
|
||||||
|
if a[i] != b[i] {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
func commonSuffixLen(a, b string) int {
|
||||||
|
i := len(a) - 1
|
||||||
|
j := len(b) - 1
|
||||||
|
count := 0
|
||||||
|
for i >= 0 && j >= 0 {
|
||||||
|
if a[i] != b[j] {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
i--
|
||||||
|
j--
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
func editDistanceWithin(a, b string, maxDistance int) bool {
|
||||||
|
if maxDistance < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if a == b {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
lenA := len(a)
|
||||||
|
lenB := len(b)
|
||||||
|
diff := lenA - lenB
|
||||||
|
if diff < 0 {
|
||||||
|
diff = -diff
|
||||||
|
}
|
||||||
|
if diff > maxDistance {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
prev := make([]int, lenB+1)
|
||||||
|
for j := 0; j <= lenB; j++ {
|
||||||
|
prev[j] = j
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 1; i <= lenA; i++ {
|
||||||
|
curr := make([]int, lenB+1)
|
||||||
|
curr[0] = i
|
||||||
|
minInRow := curr[0]
|
||||||
|
|
||||||
|
for j := 1; j <= lenB; j++ {
|
||||||
|
cost := 0
|
||||||
|
if a[i-1] != b[j-1] {
|
||||||
|
cost = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
insertCost := curr[j-1] + 1
|
||||||
|
deleteCost := prev[j] + 1
|
||||||
|
replaceCost := prev[j-1] + cost
|
||||||
|
|
||||||
|
best := insertCost
|
||||||
|
if deleteCost < best {
|
||||||
|
best = deleteCost
|
||||||
|
}
|
||||||
|
if replaceCost < best {
|
||||||
|
best = replaceCost
|
||||||
|
}
|
||||||
|
|
||||||
|
curr[j] = best
|
||||||
|
if best < minInRow {
|
||||||
|
minInRow = best
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if minInRow > maxDistance {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
prev = curr
|
||||||
|
}
|
||||||
|
|
||||||
|
return prev[lenB] <= maxDistance
|
||||||
|
}
|
||||||
|
|
||||||
func splitArtists(artists string) []string {
|
func splitArtists(artists string) []string {
|
||||||
normalized := artists
|
normalized := artists
|
||||||
normalized = strings.ReplaceAll(normalized, " feat. ", "|")
|
normalized = strings.ReplaceAll(normalized, " feat. ", "|")
|
||||||
@@ -1292,8 +1439,10 @@ func titlesMatch(expectedTitle, foundTitle string) bool {
|
|||||||
expectedLatin := isLatinScript(expectedTitle)
|
expectedLatin := isLatinScript(expectedTitle)
|
||||||
foundLatin := isLatinScript(foundTitle)
|
foundLatin := isLatinScript(foundTitle)
|
||||||
if expectedLatin != foundLatin {
|
if expectedLatin != foundLatin {
|
||||||
GoLog("[Tidal] Titles in different scripts, assuming match: '%s' vs '%s'\n", expectedTitle, foundTitle)
|
if crossScriptEquivalent(expectedTitle, foundTitle) {
|
||||||
return true
|
GoLog("[Tidal] Titles in different scripts but transliteration matched: '%s' vs '%s'\n", expectedTitle, foundTitle)
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
|
|||||||
+124
-37
@@ -43,6 +43,16 @@ class _RecentAccessView {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _CsvImportOptions {
|
||||||
|
final bool confirmed;
|
||||||
|
final bool skipDownloaded;
|
||||||
|
|
||||||
|
const _CsvImportOptions({
|
||||||
|
required this.confirmed,
|
||||||
|
required this.skipDownloaded,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class _HomeTabState extends ConsumerState<HomeTab>
|
class _HomeTabState extends ConsumerState<HomeTab>
|
||||||
with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin {
|
with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin {
|
||||||
final _urlController = TextEditingController();
|
final _urlController = TextEditingController();
|
||||||
@@ -475,19 +485,116 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
|||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
|
|
||||||
|
final options = await showDialog<_CsvImportOptions>(
|
||||||
|
context: this.context,
|
||||||
|
builder: (dialogCtx) {
|
||||||
|
var skipDownloaded = true;
|
||||||
|
return StatefulBuilder(
|
||||||
|
builder: (dialogCtx, setDialogState) => AlertDialog(
|
||||||
|
title: Text(l10n.dialogImportPlaylistTitle),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(l10n.dialogImportPlaylistMessage(tracks.length)),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
CheckboxListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: const Text('Skip already downloaded songs'),
|
||||||
|
value: skipDownloaded,
|
||||||
|
onChanged: (value) {
|
||||||
|
setDialogState(() {
|
||||||
|
skipDownloaded = value ?? true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(
|
||||||
|
dialogCtx,
|
||||||
|
const _CsvImportOptions(
|
||||||
|
confirmed: false,
|
||||||
|
skipDownloaded: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(l10n.dialogCancel),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => Navigator.pop(
|
||||||
|
dialogCtx,
|
||||||
|
_CsvImportOptions(
|
||||||
|
confirmed: true,
|
||||||
|
skipDownloaded: skipDownloaded,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(l10n.dialogImport),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (options == null || !options.confirmed) return;
|
||||||
|
|
||||||
|
var tracksToQueue = tracks;
|
||||||
|
var skippedDownloadedCount = 0;
|
||||||
|
|
||||||
|
if (options.skipDownloaded) {
|
||||||
|
final historyState = ref.read(downloadHistoryProvider);
|
||||||
|
tracksToQueue = [];
|
||||||
|
for (final track in tracks) {
|
||||||
|
final isDownloaded =
|
||||||
|
historyState.isDownloaded(track.id) ||
|
||||||
|
(track.isrc != null &&
|
||||||
|
historyState.getByIsrc(track.isrc!) != null);
|
||||||
|
if (isDownloaded) {
|
||||||
|
skippedDownloadedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
tracksToQueue.add(track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tracksToQueue.isEmpty) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(this.context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
l10n.discographySkippedDownloaded(0, skippedDownloadedCount),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final queueSnackbarMessage = skippedDownloadedCount > 0
|
||||||
|
? l10n.discographySkippedDownloaded(
|
||||||
|
tracksToQueue.length,
|
||||||
|
skippedDownloadedCount,
|
||||||
|
)
|
||||||
|
: l10n.snackbarAddedTracksToQueue(tracksToQueue.length);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
if (settings.askQualityBeforeDownload) {
|
if (settings.askQualityBeforeDownload) {
|
||||||
DownloadServicePicker.show(
|
DownloadServicePicker.show(
|
||||||
this.context,
|
this.context,
|
||||||
trackName: l10n.csvImportTracks(tracks.length),
|
trackName: l10n.csvImportTracks(tracksToQueue.length),
|
||||||
artistName: l10n.dialogImportPlaylistTitle,
|
artistName: l10n.dialogImportPlaylistTitle,
|
||||||
onSelect: (quality, service) {
|
onSelect: (quality, service) {
|
||||||
ref
|
ref.read(downloadQueueProvider.notifier).addMultipleToQueue(
|
||||||
.read(downloadQueueProvider.notifier)
|
tracksToQueue,
|
||||||
.addMultipleToQueue(tracks, service, qualityOverride: quality);
|
service,
|
||||||
|
qualityOverride: quality,
|
||||||
|
);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(this.context).showSnackBar(
|
ScaffoldMessenger.of(this.context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(l10n.snackbarAddedTracksToQueue(tracks.length)),
|
content: Text(queueSnackbarMessage),
|
||||||
action: SnackBarAction(
|
action: SnackBarAction(
|
||||||
label: l10n.snackbarViewQueue,
|
label: l10n.snackbarViewQueue,
|
||||||
onPressed: () {},
|
onPressed: () {},
|
||||||
@@ -498,39 +605,19 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final confirmed = await showDialog<bool>(
|
ref
|
||||||
context: this.context,
|
.read(downloadQueueProvider.notifier)
|
||||||
builder: (dialogCtx) => AlertDialog(
|
.addMultipleToQueue(tracksToQueue, settings.defaultService);
|
||||||
title: Text(l10n.dialogImportPlaylistTitle),
|
if (mounted) {
|
||||||
content: Text(l10n.dialogImportPlaylistMessage(tracks.length)),
|
ScaffoldMessenger.of(this.context).showSnackBar(
|
||||||
actions: [
|
SnackBar(
|
||||||
TextButton(
|
content: Text(queueSnackbarMessage),
|
||||||
onPressed: () => Navigator.pop(dialogCtx, false),
|
action: SnackBarAction(
|
||||||
child: Text(l10n.dialogCancel),
|
label: l10n.snackbarViewQueue,
|
||||||
|
onPressed: () {},
|
||||||
),
|
),
|
||||||
FilledButton(
|
),
|
||||||
onPressed: () => Navigator.pop(dialogCtx, true),
|
);
|
||||||
child: Text(l10n.dialogImport),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (confirmed == true) {
|
|
||||||
ref
|
|
||||||
.read(downloadQueueProvider.notifier)
|
|
||||||
.addMultipleToQueue(tracks, settings.defaultService);
|
|
||||||
if (mounted) {
|
|
||||||
ScaffoldMessenger.of(this.context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(l10n.snackbarAddedTracksToQueue(tracks.length)),
|
|
||||||
action: SnackBarAction(
|
|
||||||
label: l10n.snackbarViewQueue,
|
|
||||||
onPressed: () {},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user