0.5.1 - Download fixes

This commit is contained in:
exttex 2020-10-10 22:51:20 +02:00
parent 8db1223805
commit 22ceca2d9c
16 changed files with 437 additions and 91 deletions

View File

@ -1,5 +1,6 @@
package f.f.freezer; package f.f.freezer;
import android.content.Context;
import android.util.Log; import android.util.Log;
import org.jaudiotagger.audio.AudioFile; import org.jaudiotagger.audio.AudioFile;
@ -36,6 +37,13 @@ import javax.net.ssl.HttpsURLConnection;
public class Deezer { public class Deezer {
DownloadLog logger;
//Initialize for logging
void init(DownloadLog logger) {
this.logger = logger;
}
//Get guest SID cookie from deezer.com //Get guest SID cookie from deezer.com
public static String getSidCookie() throws Exception { public static String getSidCookie() throws Exception {
URL url = new URL("https://deezer.com/"); URL url = new URL("https://deezer.com/");
@ -102,7 +110,7 @@ public class Deezer {
return out; 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 //Create HEAD requests to check if exists
URL url = new URL(getTrackUrl(trackId, md5origin, mediaVersion, originalQuality)); URL url = new URL(getTrackUrl(trackId, md5origin, mediaVersion, originalQuality));
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
@ -110,6 +118,7 @@ public class Deezer {
int rc = connection.getResponseCode(); int rc = connection.getResponseCode();
//Track not available //Track not available
if (rc > 400) { if (rc > 400) {
logger.warn("Quality fallback, response code: " + Integer.toString(rc) + ", current: " + Integer.toString(originalQuality));
//Returns -1 if no quality available //Returns -1 if no quality available
if (originalQuality == 1) return -1; if (originalQuality == 1) return -1;
if (originalQuality == 3) return qualityFallback(trackId, md5origin, mediaVersion, 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)); original = original.replaceAll("%0trackNumber%", String.format("%02d", trackNumber));
//Year //Year
original = original.replaceAll("%year%", publicTrack.getString("release_date").substring(0, 4)); 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"; if (newQuality == 9) return original + ".flac";
return original + ".mp3"; return original + ".mp3";

View File

@ -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);
}
}

View File

@ -30,6 +30,8 @@ import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.util.ArrayList; import java.util.ArrayList;
import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.HttpsURLConnection;
@ -54,6 +56,7 @@ public class DownloadService extends Service {
DownloadSettings settings; DownloadSettings settings;
Context context; Context context;
SQLiteDatabase db; SQLiteDatabase db;
Deezer deezer = new Deezer();
Messenger serviceMessenger; Messenger serviceMessenger;
Messenger activityMessenger; Messenger activityMessenger;
@ -62,12 +65,11 @@ public class DownloadService extends Service {
ArrayList<Download> downloads = new ArrayList<>(); ArrayList<Download> downloads = new ArrayList<>();
ArrayList<DownloadThread> threads = new ArrayList<>(); ArrayList<DownloadThread> threads = new ArrayList<>();
ArrayList<Boolean> updateRequests = new ArrayList<>(); ArrayList<Boolean> updateRequests = new ArrayList<>();
ArrayList<String> pendingCovers = new ArrayList<>();
boolean updating = false; boolean updating = false;
Handler progressUpdateHandler = new Handler(); Handler progressUpdateHandler = new Handler();
DownloadLog logger = new DownloadLog();
public DownloadService() { public DownloadService() { }
}
@Override @Override
public void onCreate() { public void onCreate() {
@ -79,6 +81,10 @@ public class DownloadService extends Service {
createNotificationChannel(); createNotificationChannel();
createProgressUpdateHandler(); createProgressUpdateHandler();
//Setup logger, deezer api
logger.open(context);
deezer.init(logger);
//Get DB //Get DB
DownloadsDatabase dbHelper = new DownloadsDatabase(getApplicationContext()); DownloadsDatabase dbHelper = new DownloadsDatabase(getApplicationContext());
db = dbHelper.getWritableDatabase(); db = dbHelper.getWritableDatabase();
@ -89,6 +95,9 @@ public class DownloadService extends Service {
//Cancel notifications //Cancel notifications
notificationManager.cancelAll(); notificationManager.cancelAll();
//Logger
logger.close();
super.onDestroy(); super.onDestroy();
} }
@ -178,10 +187,11 @@ public class DownloadService extends Service {
//Check if last download //Check if last download
if (threads.size() == 0) { if (threads.size() == 0) {
running = false; running = false;
updateState();
return;
} }
} }
//Send updates to UI
updateProgress();
updateState();
} }
//Send state change to UI //Send state change to UI
@ -273,15 +283,16 @@ public class DownloadService extends Service {
//Quality fallback //Quality fallback
int newQuality; int newQuality;
try { 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) { } catch (Exception e) {
Log.e("QF", "Quality fallback failed: " + e.toString()); logger.error("Quality fallback failed: " + e.toString(), download);
download.state = Download.DownloadState.ERROR; download.state = Download.DownloadState.ERROR;
exit(); exit();
return; return;
} }
//No quality available //No quality available
if (newQuality == -1) { if (newQuality == -1) {
logger.error("No available quality!", download);
download.state = Download.DownloadState.DEEZER_ERROR; download.state = Download.DownloadState.DEEZER_ERROR;
exit(); exit();
return; return;
@ -294,7 +305,7 @@ public class DownloadService extends Service {
trackJson = Deezer.callPublicAPI("track", download.trackId); trackJson = Deezer.callPublicAPI("track", download.trackId);
albumJson = Deezer.callPublicAPI("album", Integer.toString(trackJson.getJSONObject("album").getInt("id"))); albumJson = Deezer.callPublicAPI("album", Integer.toString(trackJson.getJSONObject("album").getInt("id")));
} catch (Exception e) { } 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(); e.printStackTrace();
download.state = Download.DownloadState.ERROR; download.state = Download.DownloadState.ERROR;
exit(); exit();
@ -305,9 +316,8 @@ public class DownloadService extends Service {
try { try {
outFile = new File(Deezer.generateFilename(download.path, trackJson, albumJson, newQuality)); outFile = new File(Deezer.generateFilename(download.path, trackJson, albumJson, newQuality));
parentDir = new File(outFile.getParent()); parentDir = new File(outFile.getParent());
parentDir.mkdirs();
} catch (Exception e) { } 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(); e.printStackTrace();
download.state = Download.DownloadState.ERROR; download.state = Download.DownloadState.ERROR;
exit(); exit();
@ -351,7 +361,7 @@ public class DownloadService extends Service {
//Open streams //Open streams
BufferedInputStream inputStream = new BufferedInputStream(connection.getInputStream()); BufferedInputStream inputStream = new BufferedInputStream(connection.getInputStream());
OutputStream outputStream = new FileOutputStream(tmpFile.getPath()); OutputStream outputStream = new FileOutputStream(tmpFile.getPath(), true);
//Save total //Save total
download.filesize = start + connection.getContentLength(); download.filesize = start + connection.getContentLength();
//Download //Download
@ -384,7 +394,7 @@ public class DownloadService extends Service {
updateProgress(); updateProgress();
} catch (Exception e) { } catch (Exception e) {
//Download error //Download error
Log.e("DOWNLOAD", "Download error!"); logger.error("Download error: " + e.toString(), download);
e.printStackTrace(); e.printStackTrace();
download.state = Download.DownloadState.ERROR; download.state = Download.DownloadState.ERROR;
exit(); exit();
@ -397,7 +407,7 @@ public class DownloadService extends Service {
try { try {
Deezer.decryptTrack(tmpFile.getPath(), download.trackId); Deezer.decryptTrack(tmpFile.getPath(), download.trackId);
} catch (Exception e) { } catch (Exception e) {
Log.e("DEC", "Decryption failed!"); logger.error("Decryption error: " + e.toString(), download);
e.printStackTrace(); e.printStackTrace();
//Shouldn't ever fail //Shouldn't ever fail
} }
@ -408,54 +418,65 @@ public class DownloadService extends Service {
exit(); exit();
return; return;
} }
//Copy to destination directory
//Create dirs and copy
parentDir.mkdirs();
if (!tmpFile.renameTo(outFile)) { if (!tmpFile.renameTo(outFile)) {
download.state = Download.DownloadState.ERROR; boolean error = true;
exit(); try {
return; 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) { 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 { 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(); inputStream.close();
outputStream.close(); outputStream.close();
connection.disconnect(); connection.disconnect();
} catch (Exception e) { } catch (Exception ignored) {}
Log.e("ERR", "Error downloading cover!");
e.printStackTrace(); } catch (Exception e) {
coverFile.delete(); logger.error("Error downloading cover! " + e.toString(), download);
} e.printStackTrace();
//Remove lock coverFile.delete();
pendingCovers.remove(coverFile.getPath());
} }
//Tag //Tag
try { try {
Deezer.tagTrack(outFile.getPath(), trackJson, albumJson, coverFile.getPath()); Deezer.tagTrack(outFile.getPath(), trackJson, albumJson, coverFile.getPath());
@ -464,6 +485,13 @@ public class DownloadService extends Service {
e.printStackTrace(); e.printStackTrace();
} }
//Delete cover if disabled
if (!settings.trackCover)
coverFile.delete();
//Album cover
downloadAlbumCover(albumJson);
//Lyrics //Lyrics
if (settings.downloadLyrics) { if (settings.downloadLyrics) {
try { try {
@ -475,7 +503,7 @@ public class DownloadService extends Service {
fileOutputStream.write(lrcData.getBytes()); fileOutputStream.write(lrcData.getBytes());
fileOutputStream.close(); fileOutputStream.close();
} catch (Exception e) { } 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(); 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() { void stopDownload() {
stopDownload = true; stopDownload = true;
} }
@ -691,16 +759,18 @@ public class DownloadService extends Service {
int downloadThreads; int downloadThreads;
boolean overwriteDownload; boolean overwriteDownload;
boolean downloadLyrics; 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.downloadThreads = downloadThreads;
this.overwriteDownload = overwriteDownload; this.overwriteDownload = overwriteDownload;
this.downloadLyrics = downloadLyrics; this.downloadLyrics = downloadLyrics;
this.trackCover = trackCover;
} }
//Parse settings from bundle sent from UI //Parse settings from bundle sent from UI
static DownloadSettings fromBundle(Bundle b) { 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"));
} }
} }

View File

@ -66,7 +66,7 @@ public class MainActivity extends FlutterActivity {
ArrayList<HashMap> downloads = call.arguments(); ArrayList<HashMap> downloads = call.arguments();
for (int i=0; i<downloads.size(); i++) { for (int i=0; i<downloads.size(); i++) {
//Check if exists //Check if exists
Cursor cursor = db.rawQuery("SELECT id, state FROM Downloads WHERE trackId == ? AND path == ?", Cursor cursor = db.rawQuery("SELECT id, state, quality FROM Downloads WHERE trackId == ? AND path == ?",
new String[]{(String)downloads.get(i).get("trackId"), (String)downloads.get(i).get("path")}); new String[]{(String)downloads.get(i).get("trackId"), (String)downloads.get(i).get("path")});
if (cursor.getCount() > 0) { if (cursor.getCount() > 0) {
//If done or error, set state to NONE - they should be skipped because file exists //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) { if (cursor.getInt(1) >= 3) {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put("state", 0); values.put("state", 0);
values.put("quality", cursor.getInt(2));
db.update("Downloads", values, "id == ?", new String[]{Integer.toString(cursor.getInt(0))}); db.update("Downloads", values, "id == ?", new String[]{Integer.toString(cursor.getInt(0))});
Log.d("INFO", "Already exists in DB, updating to none state!"); Log.d("INFO", "Already exists in DB, updating to none state!");
} else { } else {
@ -116,6 +117,7 @@ public class MainActivity extends FlutterActivity {
bundle.putInt("downloadThreads", (int)call.argument("downloadThreads")); bundle.putInt("downloadThreads", (int)call.argument("downloadThreads"));
bundle.putBoolean("overwriteDownload", (boolean)call.argument("overwriteDownload")); bundle.putBoolean("overwriteDownload", (boolean)call.argument("overwriteDownload"));
bundle.putBoolean("downloadLyrics", (boolean)call.argument("downloadLyrics")); bundle.putBoolean("downloadLyrics", (boolean)call.argument("downloadLyrics"));
bundle.putBoolean("trackCover", (boolean)call.argument("trackCover"));
sendMessage(DownloadService.SERVICE_SETTINGS_UPDATE, bundle); sendMessage(DownloadService.SERVICE_SETTINGS_UPDATE, bundle);
result.success(null); result.success(null);

View File

@ -103,7 +103,7 @@ class Track {
} }
List<String> playbackDetails; List<String> playbackDetails;
if (mi.extras['playbackDetails'] != null) if (mi.extras['playbackDetails'] != null)
playbackDetails = jsonDecode(mi.extras['playbackDetails']).map<String>((e) => e.toString()).toList(); playbackDetails = (jsonDecode(mi.extras['playbackDetails'])??[]).map<String>((e) => e.toString()).toList();
return Track( return Track(
title: mi.title??mi.displayTitle, title: mi.title??mi.displayTitle,

View File

@ -1,8 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:disk_space/disk_space.dart'; import 'package:disk_space/disk_space.dart';
import 'package:filesize/filesize.dart'; import 'package:filesize/filesize.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/definitions.dart'; import 'package:freezer/api/definitions.dart';
@ -117,6 +119,10 @@ class DownloadManager {
Batch b = db.batch(); Batch b = db.batch();
b = await _addTrackToDB(b, track, true); b = await _addTrackToDB(b, track, true);
await b.commit(); await b.commit();
//Cache art
DefaultCacheManager().getSingleFile(track.albumArt.thumb);
DefaultCacheManager().getSingleFile(track.albumArt.full);
} }
//Get path //Get path
@ -136,6 +142,10 @@ class DownloadManager {
//Add to DB //Add to DB
if (private) { if (private) {
//Cache art
DefaultCacheManager().getSingleFile(album.art.thumb);
DefaultCacheManager().getSingleFile(album.art.full);
Batch b = db.batch(); Batch b = db.batch();
b.insert('Albums', album.toSQL(off: true), conflictAlgorithm: ConflictAlgorithm.replace); b.insert('Albums', album.toSQL(off: true), conflictAlgorithm: ConflictAlgorithm.replace);
for (Track t in album.tracks) { for (Track t in album.tracks) {
@ -168,6 +178,9 @@ class DownloadManager {
b.insert('Playlists', playlist.toSQL(), conflictAlgorithm: ConflictAlgorithm.replace); b.insert('Playlists', playlist.toSQL(), conflictAlgorithm: ConflictAlgorithm.replace);
for (Track t in playlist.tracks) { for (Track t in playlist.tracks) {
b = await _addTrackToDB(b, t, false); b = await _addTrackToDB(b, t, false);
//Cache art
DefaultCacheManager().getSingleFile(t.albumArt.thumb);
DefaultCacheManager().getSingleFile(t.albumArt.full);
} }
await b.commit(); await b.commit();
} }
@ -410,14 +423,14 @@ class DownloadManager {
path = p.join(path, sanitize(playlistName)); path = p.join(path, sanitize(playlistName));
if (settings.artistFolder) if (settings.artistFolder)
path = p.join(path, sanitize(track.artistString)); path = p.join(path, '%artist%');
//Album folder / with disk number //Album folder / with disk number
if (settings.albumFolder) { if (settings.albumFolder) {
if (settings.albumDiscFolder) { 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 { } else {
path = p.join(path, sanitize(track.album.title)); path = p.join(path, '%album%');
} }
} }
//Final path //Final path

View File

@ -166,9 +166,9 @@ const language_ar_ar = {
"Language": "اللغة", "Language": "اللغة",
"Language changed, please restart Freezer to apply!": "تم تغيير اللغة، الرجاء إعادة تشغيل فريزر لتطبيق!", "Language changed, please restart Freezer to apply!": "تم تغيير اللغة، الرجاء إعادة تشغيل فريزر لتطبيق!",
"Importing...": "جار الاستيراد...", "Importing...": "جار الاستيراد...",
"Radio": "راديو" "Radio": "راديو",
//0.5.0 Strings: //0.5.0 Strings:
"Storage permission denied!": "رفض إذن التخزين!", "Storage permission denied!": "رفض إذن التخزين!",
"Failed": "فشل", "Failed": "فشل",
"Queued": "في قائمة الانتظار", "Queued": "في قائمة الانتظار",
@ -189,7 +189,7 @@ const language_ar_ar = {
"To get latest releases": "لتنزيل اخر اصدارات البرنامج", "To get latest releases": "لتنزيل اخر اصدارات البرنامج",
"Official chat": "الدردشة الرسمية", "Official chat": "الدردشة الرسمية",
"Telegram Group": "مجموعة التلكرام", "Telegram Group": "مجموعة التلكرام",
"Huge thanks to all the contributors! <3": "شكرا جزيلا لجميع المساهمين! <3", "Huge thanks to all the contributors! <3": "<3 !شكرا جزيلا لجميع المساهمين",
"Edit playlist": "تعديل قائمة التشغيل", "Edit playlist": "تعديل قائمة التشغيل",
"Update": "تحديث", "Update": "تحديث",
"Playlist updated!": "تم تحديث قائمة التشغيل!", "Playlist updated!": "تم تحديث قائمة التشغيل!",

View File

@ -188,7 +188,9 @@ const language_en_us = {
"Storage permission denied!": "Storage permission denied!", "Storage permission denied!": "Storage permission denied!",
"Failed": "Failed", "Failed": "Failed",
"Queued": "Queued", "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", "Restart failed downloads": "Restart failed downloads",
"Clear failed": "Clear failed", "Clear failed": "Clear failed",
"Download Settings": "Download Settings", "Download Settings": "Download Settings",
@ -198,7 +200,9 @@ const language_en_us = {
"Not set": "Not set", "Not set": "Not set",
"Search or paste URL": "Search or paste URL", "Search or paste URL": "Search or paste URL",
"History": "History", "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!", "Lyrics unavailable, empty or failed to load!": "Lyrics unavailable, empty or failed to load!",
"About": "About", "About": "About",
"Telegram Channel": "Telegram Channel", "Telegram Channel": "Telegram Channel",
@ -209,6 +213,12 @@ const language_en_us = {
"Edit playlist": "Edit playlist", "Edit playlist": "Edit playlist",
"Update": "Update", "Update": "Update",
"Playlist updated!": "Playlist updated!", "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."
} }
}; };

View File

@ -187,6 +187,33 @@ const language_it_it = {
"Language changed, please restart Freezer to apply!": "Language changed, please restart Freezer to apply!":
"Lingua cambiata, riavvia Freezer per applicare la modifica!", "Lingua cambiata, riavvia Freezer per applicare la modifica!",
"Importing...": "Importando...", "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!"
} }
}; };

View File

@ -58,7 +58,8 @@ class Settings {
bool playlistFolder; bool playlistFolder;
@JsonKey(defaultValue: true) @JsonKey(defaultValue: true)
bool downloadLyrics; bool downloadLyrics;
@JsonKey(defaultValue: false)
bool trackCover;
//Appearance //Appearance
@JsonKey(defaultValue: Themes.Light) @JsonKey(defaultValue: Themes.Light)
@ -152,7 +153,8 @@ class Settings {
return { return {
"downloadThreads": downloadThreads, "downloadThreads": downloadThreads,
"overwriteDownload": overwriteDownload, "overwriteDownload": overwriteDownload,
"downloadLyrics": downloadLyrics "downloadLyrics": downloadLyrics,
"trackCover": trackCover
}; };
} }

View File

@ -33,6 +33,7 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) {
..downloadThreads = json['downloadThreads'] as int ?? 2 ..downloadThreads = json['downloadThreads'] as int ?? 2
..playlistFolder = json['playlistFolder'] as bool ?? false ..playlistFolder = json['playlistFolder'] as bool ?? false
..downloadLyrics = json['downloadLyrics'] as bool ?? true ..downloadLyrics = json['downloadLyrics'] as bool ?? true
..trackCover = json['trackCover'] as bool ?? false
..theme = ..theme =
_$enumDecodeNullable(_$ThemesEnumMap, json['theme']) ?? Themes.Light _$enumDecodeNullable(_$ThemesEnumMap, json['theme']) ?? Themes.Light
..primaryColor = Settings._colorFromJson(json['primaryColor'] as int) ..primaryColor = Settings._colorFromJson(json['primaryColor'] as int)
@ -59,6 +60,7 @@ Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
'downloadThreads': instance.downloadThreads, 'downloadThreads': instance.downloadThreads,
'playlistFolder': instance.playlistFolder, 'playlistFolder': instance.playlistFolder,
'downloadLyrics': instance.downloadLyrics, 'downloadLyrics': instance.downloadLyrics,
'trackCover': instance.trackCover,
'theme': _$ThemesEnumMap[instance.theme], 'theme': _$ThemesEnumMap[instance.theme],
'primaryColor': Settings._colorToJson(instance.primaryColor), 'primaryColor': Settings._colorToJson(instance.primaryColor),
'useArtColor': instance.useArtColor, 'useArtColor': instance.useArtColor,

View File

@ -1,12 +1,16 @@
import 'dart:async'; import 'dart:io';
import 'package:filesize/filesize.dart'; import 'package:filesize/filesize.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:freezer/api/download.dart'; import 'package:freezer/api/download.dart';
import 'package:freezer/translations.i18n.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 'cached_image.dart';
import 'dart:async';
class DownloadsScreen extends StatefulWidget { class DownloadsScreen extends StatefulWidget {
@override @override
_DownloadsScreenState createState() => _DownloadsScreenState(); _DownloadsScreenState createState() => _DownloadsScreenState();
@ -189,15 +193,30 @@ class DownloadTile extends StatelessWidget {
String subtitle() { String subtitle() {
String out = ''; String out = '';
//Download type
if (download.private) out += 'Offline'.i18n; if (download.state != DownloadState.DOWNLOADING && download.state != DownloadState.POST) {
else out += 'External'.i18n; //Download type
out += ' | '; if (download.private) out += 'Offline'.i18n;
else out += 'External'.i18n;
out += ' | ';
}
if (download.state == DownloadState.POST) {
return 'Post processing...'.i18n;
}
//Quality //Quality
if (download.quality == 9) out += 'FLAC'; if (download.quality == 9) out += 'FLAC';
if (download.quality == 3) out += 'MP3 320kbps'; if (download.quality == 3) out += 'MP3 320kbps';
if (download.quality == 1) out += 'MP3 128kbps'; 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; return out;
} }
@ -282,3 +301,62 @@ class DownloadTile extends StatelessWidget {
); );
} }
} }
class DownloadLogViewer extends StatefulWidget {
@override
_DownloadLogViewerState createState() => _DownloadLogViewerState();
}
class _DownloadLogViewerState extends State<DownloadLogViewer> {
List<String> 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])
),
),
);
},
)
);
}
}

View File

@ -203,6 +203,7 @@ class LibraryTracks extends StatefulWidget {
class _LibraryTracksState extends State<LibraryTracks> { class _LibraryTracksState extends State<LibraryTracks> {
bool _loading = false; bool _loading = false;
bool _loadingTracks = false;
ScrollController _scrollController = ScrollController(); ScrollController _scrollController = ScrollController();
List<Track> tracks = []; List<Track> tracks = [];
List<Track> allTracks = []; List<Track> allTracks = [];
@ -250,6 +251,9 @@ class _LibraryTracksState extends State<LibraryTracks> {
} }
//Load another page of tracks from deezer //Load another page of tracks from deezer
if (_loadingTracks) return;
_loadingTracks = true;
List<Track> _t; List<Track> _t;
try { try {
_t = await deezerAPI.playlistTracksPage(deezerAPI.favoritesPlaylistId, pos); _t = await deezerAPI.playlistTracksPage(deezerAPI.favoritesPlaylistId, pos);
@ -263,6 +267,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
tracks.addAll(_t); tracks.addAll(_t);
_makeFavorite(); _makeFavorite();
_loading = false; _loading = false;
_loadingTracks = false;
}); });
} }

View File

@ -88,8 +88,13 @@ class _SearchScreenState extends State<SearchScreen> {
await Future.delayed(Duration(milliseconds: 300)); await Future.delayed(Duration(milliseconds: 300));
if (q != _query) return null; if (q != _query) return null;
//Load //Load
List sugg = await deezerAPI.searchSuggestions(_query); List sugg;
setState(() => _suggestions = sugg); try {
sugg = await deezerAPI.searchSuggestions(_query);
} catch (e) {}
if (sugg != null)
setState(() => _suggestions = sugg);
} }
@override @override

View File

@ -10,6 +10,7 @@ import 'package:fluttericon/font_awesome5_icons.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/download.dart'; import 'package:freezer/api/download.dart';
import 'package:freezer/ui/downloads_screen.dart';
import 'package:freezer/ui/error.dart'; import 'package:freezer/ui/error.dart';
import 'package:freezer/ui/home_screen.dart'; import 'package:freezer/ui/home_screen.dart';
import 'package:i18n_extension/i18n_widget.dart'; import 'package:i18n_extension/i18n_widget.dart';
@ -352,7 +353,7 @@ class _QualityPickerState extends State<QualityPicker> {
} }
//Update quality in settings //Update quality in settings
void _updateQuality(AudioQuality q) { void _updateQuality(AudioQuality q) async {
setState(() { setState(() {
_quality = q; _quality = q;
}); });
@ -370,15 +371,8 @@ class _QualityPickerState extends State<QualityPicker> {
case 'offline': case 'offline':
settings.offlineQuality = _quality; break; settings.offlineQuality = _quality; break;
} }
settings.updateAudioServiceQuality(); await settings.save();
} await settings.updateAudioServiceQuality();
@override
void dispose() {
//Save
settings.updateAudioServiceQuality();
settings.save();
super.dispose();
} }
@override @override
@ -558,8 +552,9 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
if (!(await Permission.storage.request().isGranted)) return; if (!(await Permission.storage.request().isGranted)) return;
//Navigate //Navigate
Navigator.of(context).push(MaterialPageRoute( 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); setState(() => settings.downloadPath = p);
await settings.save();
},) },)
)); ));
}, },
@ -590,7 +585,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
), ),
Container(height: 8.0), Container(height: 8.0),
Text( 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( style: TextStyle(
fontSize: 12.0, fontSize: 12.0,
), ),
@ -734,6 +729,26 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
), ),
), ),
), ),
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<CreditsScreen> {
launch('https://t.me/freezerandroid'); 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(), Divider(),
...List.generate(credits.length, (i) => ListTile( ...List.generate(credits.length, (i) => ListTile(
title: Text(credits[i][0]), title: Text(credits[i][0]),

View File

@ -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. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.5.0+1 version: 0.5.1+1
environment: environment:
sdk: ">=2.8.0 <3.0.0" sdk: ">=2.8.0 <3.0.0"