From 22ceca2d9ca0917b18171aa36c412d06a4a49fe7 Mon Sep 17 00:00:00 2001 From: exttex Date: Sat, 10 Oct 2020 22:51:20 +0200 Subject: [PATCH] 0.5.1 - Download fixes --- .../app/src/main/java/f/f/freezer/Deezer.java | 12 +- .../main/java/f/f/freezer/DownloadLog.java | 99 ++++++++++ .../java/f/f/freezer/DownloadService.java | 176 ++++++++++++------ .../main/java/f/f/freezer/MainActivity.java | 4 +- lib/api/definitions.dart | 2 +- lib/api/download.dart | 19 +- lib/languages/ar_ar.dart | 8 +- lib/languages/en_us.dart | 16 +- lib/languages/it_it.dart | 29 ++- lib/settings.dart | 6 +- lib/settings.g.dart | 2 + lib/ui/downloads_screen.dart | 92 ++++++++- lib/ui/library.dart | 5 + lib/ui/search.dart | 9 +- lib/ui/settings_screen.dart | 47 +++-- pubspec.yaml | 2 +- 16 files changed, 437 insertions(+), 91 deletions(-) create mode 100644 android/app/src/main/java/f/f/freezer/DownloadLog.java diff --git a/android/app/src/main/java/f/f/freezer/Deezer.java b/android/app/src/main/java/f/f/freezer/Deezer.java index d4c8253..12fb411 100644 --- a/android/app/src/main/java/f/f/freezer/Deezer.java +++ b/android/app/src/main/java/f/f/freezer/Deezer.java @@ -1,5 +1,6 @@ package f.f.freezer; +import android.content.Context; import android.util.Log; import org.jaudiotagger.audio.AudioFile; @@ -36,6 +37,13 @@ import javax.net.ssl.HttpsURLConnection; public class Deezer { + DownloadLog logger; + + //Initialize for logging + void init(DownloadLog logger) { + this.logger = logger; + } + //Get guest SID cookie from deezer.com public static String getSidCookie() throws Exception { URL url = new URL("https://deezer.com/"); @@ -102,7 +110,7 @@ public class Deezer { return out; } - public static int qualityFallback(String trackId, String md5origin, String mediaVersion, int originalQuality) throws Exception { + public int qualityFallback(String trackId, String md5origin, String mediaVersion, int originalQuality) throws Exception { //Create HEAD requests to check if exists URL url = new URL(getTrackUrl(trackId, md5origin, mediaVersion, originalQuality)); HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); @@ -110,6 +118,7 @@ public class Deezer { int rc = connection.getResponseCode(); //Track not available if (rc > 400) { + logger.warn("Quality fallback, response code: " + Integer.toString(rc) + ", current: " + Integer.toString(originalQuality)); //Returns -1 if no quality available if (originalQuality == 1) return -1; if (originalQuality == 3) return qualityFallback(trackId, md5origin, mediaVersion, 1); @@ -251,6 +260,7 @@ public class Deezer { original = original.replaceAll("%0trackNumber%", String.format("%02d", trackNumber)); //Year original = original.replaceAll("%year%", publicTrack.getString("release_date").substring(0, 4)); + original = original.replaceAll("%date%", publicTrack.getString("release_date")); if (newQuality == 9) return original + ".flac"; return original + ".mp3"; diff --git a/android/app/src/main/java/f/f/freezer/DownloadLog.java b/android/app/src/main/java/f/f/freezer/DownloadLog.java new file mode 100644 index 0000000..4c38bdb --- /dev/null +++ b/android/app/src/main/java/f/f/freezer/DownloadLog.java @@ -0,0 +1,99 @@ +package f.f.freezer; + +import android.content.Context; +import android.util.Log; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.text.SimpleDateFormat; +import java.util.Calendar; + +public class DownloadLog { + + BufferedWriter writer; + + //Open/Create file + public void open(Context context) { + File file = new File(context.getExternalFilesDir(""), "download.log"); + try { + if (!file.exists()) { + file.createNewFile(); + } + writer = new BufferedWriter(new FileWriter(file, true)); + } catch (Exception ignored) { + Log.e("DOWN", "Error opening download log!"); + } + } + + //Close log + public void close() { + try { + writer.close(); + } catch (Exception ignored) { + Log.w("DOWN", "Error closing download log!"); + } + } + + public String time() { + SimpleDateFormat format = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss"); + return format.format(Calendar.getInstance().getTime()); + } + + //Write error to log + public void error(String info) { + if (writer == null) return; + String data = "E:" + time() + ": " + info; + try { + writer.write(data); + writer.newLine(); + writer.flush(); + } catch (Exception ignored) { + Log.w("DOWN", "Error writing into log."); + } + Log.e("DOWN", data); + } + + //Write error to log with download info + public void error(String info, Download download) { + if (writer == null) return; + String data = "E:" + time() + " (TrackID: " + download.trackId + ", ID: " + Integer.toString(download.id) + "): " +info; + try { + writer.write(data); + writer.newLine(); + writer.flush(); + } catch (Exception ignored) { + Log.w("DOWN", "Error writing into log."); + } + Log.e("DOWN", data); + } + + //Write warning to log + public void warn(String info) { + if (writer == null) return; + String data = "W:" + time() + ": " + info; + try { + writer.write(data); + writer.newLine(); + writer.flush(); + } catch (Exception ignored) { + Log.w("DOWN", "Error writing into log."); + } + Log.w("DOWN", data); + } + + //Write warning to log with download info + public void warn(String info, Download download) { + if (writer == null) return; + String data = "W:" + time() + " (TrackID: " + download.trackId + ", ID: " + Integer.toString(download.id) + "): " +info; + try { + writer.write(data); + writer.newLine(); + writer.flush(); + } catch (Exception ignored) { + Log.w("DOWN", "Error writing into log."); + } + Log.w("DOWN", data); + } + +} diff --git a/android/app/src/main/java/f/f/freezer/DownloadService.java b/android/app/src/main/java/f/f/freezer/DownloadService.java index 2633c1a..36fd6d1 100644 --- a/android/app/src/main/java/f/f/freezer/DownloadService.java +++ b/android/app/src/main/java/f/f/freezer/DownloadService.java @@ -30,6 +30,8 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.text.DecimalFormat; import java.util.ArrayList; import javax.net.ssl.HttpsURLConnection; @@ -54,6 +56,7 @@ public class DownloadService extends Service { DownloadSettings settings; Context context; SQLiteDatabase db; + Deezer deezer = new Deezer(); Messenger serviceMessenger; Messenger activityMessenger; @@ -62,12 +65,11 @@ public class DownloadService extends Service { ArrayList downloads = new ArrayList<>(); ArrayList threads = new ArrayList<>(); ArrayList updateRequests = new ArrayList<>(); - ArrayList pendingCovers = new ArrayList<>(); boolean updating = false; Handler progressUpdateHandler = new Handler(); + DownloadLog logger = new DownloadLog(); - public DownloadService() { - } + public DownloadService() { } @Override public void onCreate() { @@ -79,6 +81,10 @@ public class DownloadService extends Service { createNotificationChannel(); createProgressUpdateHandler(); + //Setup logger, deezer api + logger.open(context); + deezer.init(logger); + //Get DB DownloadsDatabase dbHelper = new DownloadsDatabase(getApplicationContext()); db = dbHelper.getWritableDatabase(); @@ -89,6 +95,9 @@ public class DownloadService extends Service { //Cancel notifications notificationManager.cancelAll(); + //Logger + logger.close(); + super.onDestroy(); } @@ -178,10 +187,11 @@ public class DownloadService extends Service { //Check if last download if (threads.size() == 0) { running = false; - updateState(); - return; } } + //Send updates to UI + updateProgress(); + updateState(); } //Send state change to UI @@ -273,15 +283,16 @@ public class DownloadService extends Service { //Quality fallback int newQuality; try { - newQuality = Deezer.qualityFallback(download.trackId, download.md5origin, download.mediaVersion, download.quality); + newQuality = deezer.qualityFallback(download.trackId, download.md5origin, download.mediaVersion, download.quality); } catch (Exception e) { - Log.e("QF", "Quality fallback failed: " + e.toString()); + logger.error("Quality fallback failed: " + e.toString(), download); download.state = Download.DownloadState.ERROR; exit(); return; } //No quality available if (newQuality == -1) { + logger.error("No available quality!", download); download.state = Download.DownloadState.DEEZER_ERROR; exit(); return; @@ -294,7 +305,7 @@ public class DownloadService extends Service { trackJson = Deezer.callPublicAPI("track", download.trackId); albumJson = Deezer.callPublicAPI("album", Integer.toString(trackJson.getJSONObject("album").getInt("id"))); } catch (Exception e) { - Log.e("ERR", "Unable to fetch track metadata."); + logger.error("Unable to fetch track and album metadata! " + e.toString(), download); e.printStackTrace(); download.state = Download.DownloadState.ERROR; exit(); @@ -305,9 +316,8 @@ public class DownloadService extends Service { try { outFile = new File(Deezer.generateFilename(download.path, trackJson, albumJson, newQuality)); parentDir = new File(outFile.getParent()); - parentDir.mkdirs(); } catch (Exception e) { - Log.e("ERR", "Error creating directories! TrackID: " + download.trackId); + logger.error("Error generating track filename (" + download.path + "): " + e.toString(), download); e.printStackTrace(); download.state = Download.DownloadState.ERROR; exit(); @@ -351,7 +361,7 @@ public class DownloadService extends Service { //Open streams BufferedInputStream inputStream = new BufferedInputStream(connection.getInputStream()); - OutputStream outputStream = new FileOutputStream(tmpFile.getPath()); + OutputStream outputStream = new FileOutputStream(tmpFile.getPath(), true); //Save total download.filesize = start + connection.getContentLength(); //Download @@ -384,7 +394,7 @@ public class DownloadService extends Service { updateProgress(); } catch (Exception e) { //Download error - Log.e("DOWNLOAD", "Download error!"); + logger.error("Download error: " + e.toString(), download); e.printStackTrace(); download.state = Download.DownloadState.ERROR; exit(); @@ -397,7 +407,7 @@ public class DownloadService extends Service { try { Deezer.decryptTrack(tmpFile.getPath(), download.trackId); } catch (Exception e) { - Log.e("DEC", "Decryption failed!"); + logger.error("Decryption error: " + e.toString(), download); e.printStackTrace(); //Shouldn't ever fail } @@ -408,54 +418,65 @@ public class DownloadService extends Service { exit(); return; } - //Copy to destination directory + + //Create dirs and copy + parentDir.mkdirs(); if (!tmpFile.renameTo(outFile)) { - download.state = Download.DownloadState.ERROR; - exit(); - return; + boolean error = true; + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Files.move(tmpFile.toPath(), outFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + tmpFile.delete(); + error = false; + } + } catch (Exception e) { + logger.error("Error moving file! " + outFile.getPath() + ", " + e.toString(), download); + download.state = Download.DownloadState.ERROR; + exit(); + return; + } + if (error) { + logger.error("Error moving file! " + outFile.getPath(), download); + download.state = Download.DownloadState.ERROR; + exit(); + return; + } } if (!download.priv) { - //Download cover - File coverFile = new File(parentDir, "cover.jpg"); - //Wait for another thread to download it - while (pendingCovers.contains(coverFile.getPath())) { - try { Thread.sleep(100); } catch (Exception ignored) {} - } - if (!coverFile.exists()) { + //Download cover for each track + File coverFile = new File(outFile.getPath().substring(0, outFile.getPath().lastIndexOf('.')) + ".jpg"); + + try { + URL url = new URL("http://e-cdn-images.deezer.com/images/cover/" + trackJson.getString("md5_image") + "/1400x1400-000000-80-0-0.jpg"); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + //Set headers + connection.setRequestMethod("GET"); + connection.connect(); + //Open streams + InputStream inputStream = connection.getInputStream(); + OutputStream outputStream = new FileOutputStream(coverFile.getPath()); + //Download + byte[] buffer = new byte[4096]; + int read = 0; + while ((read = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, read); + } + //On done try { - //Create fake file so other threads don't start downloading covers - coverFile.createNewFile(); - pendingCovers.add(coverFile.getPath()); - - URL url = new URL("http://e-cdn-images.deezer.com/images/cover/" + trackJson.getString("md5_image") + "/1400x1400-000000-80-0-0.jpg"); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - //Set headers - connection.setRequestMethod("GET"); - connection.connect(); - //Open streams - InputStream inputStream = connection.getInputStream(); - OutputStream outputStream = new FileOutputStream(coverFile.getPath()); - //Download - byte[] buffer = new byte[4096]; - int read = 0; - while ((read = inputStream.read(buffer)) != -1) { - outputStream.write(buffer, 0, read); - } - //On done inputStream.close(); outputStream.close(); connection.disconnect(); - } catch (Exception e) { - Log.e("ERR", "Error downloading cover!"); - e.printStackTrace(); - coverFile.delete(); - } - //Remove lock - pendingCovers.remove(coverFile.getPath()); + } catch (Exception ignored) {} + + } catch (Exception e) { + logger.error("Error downloading cover! " + e.toString(), download); + e.printStackTrace(); + coverFile.delete(); } + //Tag try { Deezer.tagTrack(outFile.getPath(), trackJson, albumJson, coverFile.getPath()); @@ -464,6 +485,13 @@ public class DownloadService extends Service { e.printStackTrace(); } + //Delete cover if disabled + if (!settings.trackCover) + coverFile.delete(); + + //Album cover + downloadAlbumCover(albumJson); + //Lyrics if (settings.downloadLyrics) { try { @@ -475,7 +503,7 @@ public class DownloadService extends Service { fileOutputStream.write(lrcData.getBytes()); fileOutputStream.close(); } catch (Exception e) { - Log.w("WAR", "Missing lyrics! " + e.toString()); + logger.warn("Error downloading lyrics! " + e.toString(), download); } } } @@ -486,6 +514,46 @@ public class DownloadService extends Service { stopSelf(); } + //Each track has own album art, this is to download cover.jpg + void downloadAlbumCover(JSONObject albumJson) { + //Checks + if (albumJson == null || !albumJson.has("md5_image")) return; + File coverFile = new File(parentDir, "cover.jpg"); + if (coverFile.exists()) return; + //Don't download if doesn't have album + if (!download.path.contains("/%album%/")) return; + + try { + //Create to lock + coverFile.createNewFile(); + + URL url = new URL("http://e-cdn-images.deezer.com/images/cover/" + albumJson.getString("md5_image") + "/1400x1400-000000-80-0-0.jpg"); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + //Set headers + connection.setRequestMethod("GET"); + connection.connect(); + //Open streams + InputStream inputStream = connection.getInputStream(); + OutputStream outputStream = new FileOutputStream(coverFile.getPath()); + //Download + byte[] buffer = new byte[4096]; + int read = 0; + while ((read = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, read); + } + //On done + try { + inputStream.close(); + outputStream.close(); + connection.disconnect(); + } catch (Exception ignored) {} + + } catch (Exception e) { + logger.warn("Error downloading album cover! " + e.toString(), download); + coverFile.delete(); + } + } + void stopDownload() { stopDownload = true; } @@ -691,16 +759,18 @@ public class DownloadService extends Service { int downloadThreads; boolean overwriteDownload; boolean downloadLyrics; + boolean trackCover; - private DownloadSettings(int downloadThreads, boolean overwriteDownload, boolean downloadLyrics) { + private DownloadSettings(int downloadThreads, boolean overwriteDownload, boolean downloadLyrics, boolean trackCover) { this.downloadThreads = downloadThreads; this.overwriteDownload = overwriteDownload; this.downloadLyrics = downloadLyrics; + this.trackCover = trackCover; } //Parse settings from bundle sent from UI static DownloadSettings fromBundle(Bundle b) { - return new DownloadSettings(b.getInt("downloadThreads"), b.getBoolean("overwriteDownload"), b.getBoolean("downloadLyrics")); + return new DownloadSettings(b.getInt("downloadThreads"), b.getBoolean("overwriteDownload"), b.getBoolean("downloadLyrics"), b.getBoolean("trackCover")); } } diff --git a/android/app/src/main/java/f/f/freezer/MainActivity.java b/android/app/src/main/java/f/f/freezer/MainActivity.java index 2b1c32f..1faebd5 100644 --- a/android/app/src/main/java/f/f/freezer/MainActivity.java +++ b/android/app/src/main/java/f/f/freezer/MainActivity.java @@ -66,7 +66,7 @@ public class MainActivity extends FlutterActivity { ArrayList downloads = call.arguments(); for (int i=0; i 0) { //If done or error, set state to NONE - they should be skipped because file exists @@ -74,6 +74,7 @@ public class MainActivity extends FlutterActivity { if (cursor.getInt(1) >= 3) { ContentValues values = new ContentValues(); values.put("state", 0); + values.put("quality", cursor.getInt(2)); db.update("Downloads", values, "id == ?", new String[]{Integer.toString(cursor.getInt(0))}); Log.d("INFO", "Already exists in DB, updating to none state!"); } else { @@ -116,6 +117,7 @@ public class MainActivity extends FlutterActivity { bundle.putInt("downloadThreads", (int)call.argument("downloadThreads")); bundle.putBoolean("overwriteDownload", (boolean)call.argument("overwriteDownload")); bundle.putBoolean("downloadLyrics", (boolean)call.argument("downloadLyrics")); + bundle.putBoolean("trackCover", (boolean)call.argument("trackCover")); sendMessage(DownloadService.SERVICE_SETTINGS_UPDATE, bundle); result.success(null); diff --git a/lib/api/definitions.dart b/lib/api/definitions.dart index 7a3ba4e..b7e8706 100644 --- a/lib/api/definitions.dart +++ b/lib/api/definitions.dart @@ -103,7 +103,7 @@ class Track { } List playbackDetails; if (mi.extras['playbackDetails'] != null) - playbackDetails = jsonDecode(mi.extras['playbackDetails']).map((e) => e.toString()).toList(); + playbackDetails = (jsonDecode(mi.extras['playbackDetails'])??[]).map((e) => e.toString()).toList(); return Track( title: mi.title??mi.displayTitle, diff --git a/lib/api/download.dart b/lib/api/download.dart index 35f87e4..8d31580 100644 --- a/lib/api/download.dart +++ b/lib/api/download.dart @@ -1,8 +1,10 @@ import 'dart:async'; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:disk_space/disk_space.dart'; import 'package:filesize/filesize.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/definitions.dart'; @@ -117,6 +119,10 @@ class DownloadManager { Batch b = db.batch(); b = await _addTrackToDB(b, track, true); await b.commit(); + + //Cache art + DefaultCacheManager().getSingleFile(track.albumArt.thumb); + DefaultCacheManager().getSingleFile(track.albumArt.full); } //Get path @@ -136,6 +142,10 @@ class DownloadManager { //Add to DB if (private) { + //Cache art + DefaultCacheManager().getSingleFile(album.art.thumb); + DefaultCacheManager().getSingleFile(album.art.full); + Batch b = db.batch(); b.insert('Albums', album.toSQL(off: true), conflictAlgorithm: ConflictAlgorithm.replace); for (Track t in album.tracks) { @@ -168,6 +178,9 @@ class DownloadManager { b.insert('Playlists', playlist.toSQL(), conflictAlgorithm: ConflictAlgorithm.replace); for (Track t in playlist.tracks) { b = await _addTrackToDB(b, t, false); + //Cache art + DefaultCacheManager().getSingleFile(t.albumArt.thumb); + DefaultCacheManager().getSingleFile(t.albumArt.full); } await b.commit(); } @@ -410,14 +423,14 @@ class DownloadManager { path = p.join(path, sanitize(playlistName)); if (settings.artistFolder) - path = p.join(path, sanitize(track.artistString)); + path = p.join(path, '%artist%'); //Album folder / with disk number if (settings.albumFolder) { if (settings.albumDiscFolder) { - path = p.join(path, sanitize(track.album.title) + ' - Disk ' + track.diskNumber.toString()); + path = p.join(path, '%album%' + ' - Disk ' + track.diskNumber.toString()); } else { - path = p.join(path, sanitize(track.album.title)); + path = p.join(path, '%album%'); } } //Final path diff --git a/lib/languages/ar_ar.dart b/lib/languages/ar_ar.dart index 23535a7..72cd715 100644 --- a/lib/languages/ar_ar.dart +++ b/lib/languages/ar_ar.dart @@ -166,9 +166,9 @@ const language_ar_ar = { "Language": "اللغة", "Language changed, please restart Freezer to apply!": "تم تغيير اللغة، الرجاء إعادة تشغيل فريزر لتطبيق!", "Importing...": "جار الاستيراد...", - "Radio": "راديو" - - //0.5.0 Strings: + "Radio": "راديو", + + //0.5.0 Strings: "Storage permission denied!": "رفض إذن التخزين!", "Failed": "فشل", "Queued": "في قائمة الانتظار", @@ -189,7 +189,7 @@ const language_ar_ar = { "To get latest releases": "لتنزيل اخر اصدارات البرنامج", "Official chat": "الدردشة الرسمية", "Telegram Group": "مجموعة التلكرام", - "Huge thanks to all the contributors! <3": "شكرا جزيلا لجميع المساهمين! <3", + "Huge thanks to all the contributors! <3": "<3 !شكرا جزيلا لجميع المساهمين", "Edit playlist": "تعديل قائمة التشغيل", "Update": "تحديث", "Playlist updated!": "تم تحديث قائمة التشغيل!", diff --git a/lib/languages/en_us.dart b/lib/languages/en_us.dart index 0ee05ff..45eed7c 100644 --- a/lib/languages/en_us.dart +++ b/lib/languages/en_us.dart @@ -188,7 +188,9 @@ const language_en_us = { "Storage permission denied!": "Storage permission denied!", "Failed": "Failed", "Queued": "Queued", - "External": "External", + //Updated in 0.5.1 - used in context of download: + "External": "Storage", + //0.5.0 "Restart failed downloads": "Restart failed downloads", "Clear failed": "Clear failed", "Download Settings": "Download Settings", @@ -198,7 +200,9 @@ const language_en_us = { "Not set": "Not set", "Search or paste URL": "Search or paste URL", "History": "History", - "Download threads": "Download threads", + //Updated 0.5.1 + "Download threads": "Concurrent downloads", + //0.5.0 "Lyrics unavailable, empty or failed to load!": "Lyrics unavailable, empty or failed to load!", "About": "About", "Telegram Channel": "Telegram Channel", @@ -209,6 +213,12 @@ const language_en_us = { "Edit playlist": "Edit playlist", "Update": "Update", "Playlist updated!": "Playlist updated!", - "Downloads added!": "Downloads added!" + "Downloads added!": "Downloads added!", + + //0.5.1 Strings: + "Save cover file for every track": "Save cover file for every track", + "Download Log": "Download Log", + "Repository": "Repository", + "Source code, report issues there.": "Source code, report issues there." } }; diff --git a/lib/languages/it_it.dart b/lib/languages/it_it.dart index 8ecc837..e21bba9 100644 --- a/lib/languages/it_it.dart +++ b/lib/languages/it_it.dart @@ -187,6 +187,33 @@ const language_it_it = { "Language changed, please restart Freezer to apply!": "Lingua cambiata, riavvia Freezer per applicare la modifica!", "Importing...": "Importando...", - "Radio": "Radio" + "Radio": "Radio", + + //0.5.0 Strings: + "Storage permission denied!": "Autorizzazione di archiviazione negata!", + "Failed": "Fallito", + "Queued": "In coda", + "External": "Esterno", + "Restart failed downloads": "Riavvia download non riusciti", + "Clear failed": "Pulisci fallito", + "Download Settings": "Scarica le impostazioni", + "Create folder for playlist": "Crea cartella per playlist", + "Download .LRC lyrics": "Scarica testi .LRC", + "Proxy": "Proxy", + "Not set": "Non impostato", + "Search or paste URL": "Cerca o incolla l'URL", + "History": "Storia", + "Download threads": "Scarica threads", + "Lyrics unavailable, empty or failed to load!": "Testi non disponibili, vuoti o caricamento non riuscito!", + "About": "Info", + "Telegram Channel": "Canale Telegram", + "To get latest releases": "Per ottenere le ultime versioni", + "Official chat": "Chat ufficiale", + "Telegram Group": "Gruppo Telegram", + "Huge thanks to all the contributors! <3": "Un enorme grazie a tutti i collaboratori! <3", + "Edit playlist": "Modifica playlist", + "Update": "Aggiorna", + "Playlist updated!": "Playlist aggiornata!", + "Downloads added!": "Download aggiunti!" } }; diff --git a/lib/settings.dart b/lib/settings.dart index 03d0efb..dc1e359 100644 --- a/lib/settings.dart +++ b/lib/settings.dart @@ -58,7 +58,8 @@ class Settings { bool playlistFolder; @JsonKey(defaultValue: true) bool downloadLyrics; - + @JsonKey(defaultValue: false) + bool trackCover; //Appearance @JsonKey(defaultValue: Themes.Light) @@ -152,7 +153,8 @@ class Settings { return { "downloadThreads": downloadThreads, "overwriteDownload": overwriteDownload, - "downloadLyrics": downloadLyrics + "downloadLyrics": downloadLyrics, + "trackCover": trackCover }; } diff --git a/lib/settings.g.dart b/lib/settings.g.dart index 14f301d..c2f1274 100644 --- a/lib/settings.g.dart +++ b/lib/settings.g.dart @@ -33,6 +33,7 @@ Settings _$SettingsFromJson(Map json) { ..downloadThreads = json['downloadThreads'] as int ?? 2 ..playlistFolder = json['playlistFolder'] as bool ?? false ..downloadLyrics = json['downloadLyrics'] as bool ?? true + ..trackCover = json['trackCover'] as bool ?? false ..theme = _$enumDecodeNullable(_$ThemesEnumMap, json['theme']) ?? Themes.Light ..primaryColor = Settings._colorFromJson(json['primaryColor'] as int) @@ -59,6 +60,7 @@ Map _$SettingsToJson(Settings instance) => { 'downloadThreads': instance.downloadThreads, 'playlistFolder': instance.playlistFolder, 'downloadLyrics': instance.downloadLyrics, + 'trackCover': instance.trackCover, 'theme': _$ThemesEnumMap[instance.theme], 'primaryColor': Settings._colorToJson(instance.primaryColor), 'useArtColor': instance.useArtColor, diff --git a/lib/ui/downloads_screen.dart b/lib/ui/downloads_screen.dart index fbc1e01..da4091d 100644 --- a/lib/ui/downloads_screen.dart +++ b/lib/ui/downloads_screen.dart @@ -1,12 +1,16 @@ -import 'dart:async'; +import 'dart:io'; import 'package:filesize/filesize.dart'; import 'package:flutter/material.dart'; import 'package:freezer/api/download.dart'; import 'package:freezer/translations.i18n.dart'; - +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; import 'cached_image.dart'; +import 'dart:async'; + + class DownloadsScreen extends StatefulWidget { @override _DownloadsScreenState createState() => _DownloadsScreenState(); @@ -189,15 +193,30 @@ class DownloadTile extends StatelessWidget { String subtitle() { String out = ''; - //Download type - if (download.private) out += 'Offline'.i18n; - else out += 'External'.i18n; - out += ' | '; + + if (download.state != DownloadState.DOWNLOADING && download.state != DownloadState.POST) { + //Download type + if (download.private) out += 'Offline'.i18n; + else out += 'External'.i18n; + out += ' | '; + } + + if (download.state == DownloadState.POST) { + return 'Post processing...'.i18n; + } + //Quality if (download.quality == 9) out += 'FLAC'; if (download.quality == 3) out += 'MP3 320kbps'; if (download.quality == 1) out += 'MP3 128kbps'; + //Downloading show progress + if (download.state == DownloadState.DOWNLOADING) { + out += ' | ${filesize(download.received, 2)} / ${filesize(download.filesize, 2)}'; + double progress = download.received.toDouble() / download.filesize.toDouble(); + out += ' ${(progress*100.0).toStringAsFixed(2)}%'; + } + return out; } @@ -281,4 +300,63 @@ class DownloadTile extends StatelessWidget { ], ); } -} \ No newline at end of file +} + +class DownloadLogViewer extends StatefulWidget { + @override + _DownloadLogViewerState createState() => _DownloadLogViewerState(); +} + +class _DownloadLogViewerState extends State { + + List data = []; + + //Load log from file + Future _load() async { + String path = p.join((await getExternalStorageDirectory()).path, 'download.log'); + File file = File(path); + if (await file.exists()) { + String _d = await file.readAsString(); + setState(() { + data = _d.replaceAll("\r", "").split("\n"); + }); + } + } + + //Get color by log type + Color color(String line) { + if (line.startsWith('E:')) return Colors.red; + if (line.startsWith('W:')) return Colors.orange[600]; + return null; + } + + @override + void initState() { + _load(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Download Log'.i18n), + ), + body: ListView.builder( + itemCount: data.length, + itemBuilder: (context, i) { + return Padding( + padding: EdgeInsets.all(8.0), + child: Text( + data[i], + style: TextStyle( + fontSize: 14.0, + color: color(data[i]) + ), + ), + ); + }, + ) + ); + } +} diff --git a/lib/ui/library.dart b/lib/ui/library.dart index 5eefa40..2d2327e 100644 --- a/lib/ui/library.dart +++ b/lib/ui/library.dart @@ -203,6 +203,7 @@ class LibraryTracks extends StatefulWidget { class _LibraryTracksState extends State { bool _loading = false; + bool _loadingTracks = false; ScrollController _scrollController = ScrollController(); List tracks = []; List allTracks = []; @@ -250,6 +251,9 @@ class _LibraryTracksState extends State { } //Load another page of tracks from deezer + if (_loadingTracks) return; + _loadingTracks = true; + List _t; try { _t = await deezerAPI.playlistTracksPage(deezerAPI.favoritesPlaylistId, pos); @@ -263,6 +267,7 @@ class _LibraryTracksState extends State { tracks.addAll(_t); _makeFavorite(); _loading = false; + _loadingTracks = false; }); } diff --git a/lib/ui/search.dart b/lib/ui/search.dart index 825bf2c..471c1bd 100644 --- a/lib/ui/search.dart +++ b/lib/ui/search.dart @@ -88,8 +88,13 @@ class _SearchScreenState extends State { await Future.delayed(Duration(milliseconds: 300)); if (q != _query) return null; //Load - List sugg = await deezerAPI.searchSuggestions(_query); - setState(() => _suggestions = sugg); + List sugg; + try { + sugg = await deezerAPI.searchSuggestions(_query); + } catch (e) {} + + if (sugg != null) + setState(() => _suggestions = sugg); } @override diff --git a/lib/ui/settings_screen.dart b/lib/ui/settings_screen.dart index 4240860..496f566 100644 --- a/lib/ui/settings_screen.dart +++ b/lib/ui/settings_screen.dart @@ -10,6 +10,7 @@ import 'package:fluttericon/font_awesome5_icons.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/download.dart'; +import 'package:freezer/ui/downloads_screen.dart'; import 'package:freezer/ui/error.dart'; import 'package:freezer/ui/home_screen.dart'; import 'package:i18n_extension/i18n_widget.dart'; @@ -352,7 +353,7 @@ class _QualityPickerState extends State { } //Update quality in settings - void _updateQuality(AudioQuality q) { + void _updateQuality(AudioQuality q) async { setState(() { _quality = q; }); @@ -370,15 +371,8 @@ class _QualityPickerState extends State { case 'offline': settings.offlineQuality = _quality; break; } - settings.updateAudioServiceQuality(); - } - - @override - void dispose() { - //Save - settings.updateAudioServiceQuality(); - settings.save(); - super.dispose(); + await settings.save(); + await settings.updateAudioServiceQuality(); } @override @@ -558,8 +552,9 @@ class _DownloadsSettingsState extends State { if (!(await Permission.storage.request().isGranted)) return; //Navigate Navigator.of(context).push(MaterialPageRoute( - builder: (context) => DirectoryPicker(settings.downloadPath, onSelect: (String p) { + builder: (context) => DirectoryPicker(settings.downloadPath, onSelect: (String p) async { setState(() => settings.downloadPath = p); + await settings.save(); },) )); }, @@ -590,7 +585,7 @@ class _DownloadsSettingsState extends State { ), Container(height: 8.0), Text( - 'Valid variables are'.i18n + ': %artists%, %artist%, %title%, %album%, %trackNumber%, %0trackNumber%, %feats%, %playlistTrackNumber%, %0playlistTrackNumber%, %year%', + 'Valid variables are'.i18n + ': %artists%, %artist%, %title%, %album%, %trackNumber%, %0trackNumber%, %feats%, %playlistTrackNumber%, %0playlistTrackNumber%, %year%, %date%', style: TextStyle( fontSize: 12.0, ), @@ -734,6 +729,26 @@ class _DownloadsSettingsState extends State { ), ), ), + ListTile( + title: Text('Save cover file for every track'.i18n), + leading: Container( + width: 30.0, + child: Checkbox( + value: settings.trackCover, + onChanged: (v) { + setState(() => settings.trackCover = v); + settings.save(); + }, + ), + ), + ), + ListTile( + title: Text('Download Log'.i18n), + leading: Icon(Icons.sticky_note_2), + onTap: () => Navigator.of(context).push( + MaterialPageRoute(builder: (context) => DownloadLogViewer()) + ), + ) ], ), ); @@ -1071,6 +1086,14 @@ class _CreditsScreenState extends State { launch('https://t.me/freezerandroid'); }, ), + ListTile( + title: Text('Repository'.i18n), + subtitle: Text('Source code, report issues there.'), + leading: Icon(Icons.code, color: Colors.green, size: 36.0), + onTap: () { + launch('https://notabug.org/exttex/freezer'); + }, + ), Divider(), ...List.generate(credits.length, (i) => ListTile( title: Text(credits[i][0]), diff --git a/pubspec.yaml b/pubspec.yaml index 22e26d7..37ba240 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.5.0+1 +version: 0.5.1+1 environment: sdk: ">=2.8.0 <3.0.0"