mirror of
https://github.com/zoriya/Opus.git
synced 2025-12-06 06:26:15 +00:00
474 lines
18 KiB
C#
474 lines
18 KiB
C#
using Android;
|
|
using Android.App;
|
|
using Android.Content;
|
|
using Android.Content.PM;
|
|
using Android.Database;
|
|
using Android.Media;
|
|
using Android.Net;
|
|
using Android.OS;
|
|
using Android.Provider;
|
|
using Android.Support.V4.App;
|
|
using Android.Support.V7.Preferences;
|
|
using Android.Support.V7.Widget;
|
|
using Android.Views;
|
|
using Android.Widget;
|
|
using MusicApp.Resources.values;
|
|
using SQLite;
|
|
using Square.Picasso;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using TagLib;
|
|
using YoutubeExplode;
|
|
using YoutubeExplode.Models;
|
|
using YoutubeExplode.Models.MediaStreams;
|
|
using static Android.Provider.MediaStore.Audio;
|
|
using Bitmap = Android.Graphics.Bitmap;
|
|
using CursorLoader = Android.Support.V4.Content.CursorLoader;
|
|
using File = System.IO.File;
|
|
using Stream = System.IO.Stream;
|
|
|
|
namespace MusicApp.Resources.Portable_Class
|
|
{
|
|
[Service]
|
|
public class Downloader : Service, MediaScannerConnection.IOnScanCompletedListener
|
|
{
|
|
public static Downloader instance;
|
|
public string downloadPath;
|
|
public int maxDownload = 4;
|
|
public static List<DownloadFile> queue = new List<DownloadFile>();
|
|
|
|
public int currentStrike = 0;
|
|
private static int downloadCount = 0;
|
|
private static List<string> files = new List<string>();
|
|
private NotificationCompat.Builder notification;
|
|
private CancellationTokenSource cancellation = new CancellationTokenSource();
|
|
private const int notificationID = 1001;
|
|
private const int RequestCode = 5465;
|
|
|
|
|
|
public override IBinder OnBind(Intent intent)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
public override void OnCreate()
|
|
{
|
|
base.OnCreate();
|
|
}
|
|
|
|
public override void OnDestroy()
|
|
{
|
|
instance = null;
|
|
queue.Clear();
|
|
}
|
|
|
|
public override StartCommandResult OnStartCommand(Intent intent, StartCommandFlags flags, int startId)
|
|
{
|
|
if(intent != null && intent.Action == "Cancel")
|
|
{
|
|
Cancel();
|
|
return StartCommandResult.NotSticky;
|
|
}
|
|
|
|
instance = this;
|
|
return StartCommandResult.Sticky;
|
|
}
|
|
|
|
public async void StartDownload()
|
|
{
|
|
while(downloadCount < maxDownload && queue.Count(x => x.State == DownloadState.None) > 0)
|
|
{
|
|
#pragma warning disable CS4014
|
|
Task.Run(() => { DownloadAudio(queue.FindIndex(x => x.State == DownloadState.None), downloadPath); }, cancellation.Token);
|
|
await Task.Delay(10);
|
|
}
|
|
}
|
|
|
|
private async void DownloadAudio(int position, string path)
|
|
{
|
|
if (position < 0 || position > queue.Count || queue[position].State != DownloadState.None)
|
|
return;
|
|
|
|
queue[position].State = DownloadState.Initialization;
|
|
UpdateList(position);
|
|
const string permission = Manifest.Permission.WriteExternalStorage;
|
|
if (Android.Support.V4.Content.ContextCompat.CheckSelfPermission(this, permission) != (int)Permission.Granted)
|
|
{
|
|
string[] permissions = new string[] { permission };
|
|
MainActivity.instance.RequestPermissions(permissions, RequestCode);
|
|
|
|
await Task.Delay(1000);
|
|
while (Android.Support.V4.Content.ContextCompat.CheckSelfPermission(this, permission) != (int)Permission.Granted)
|
|
await Task.Delay(500);
|
|
}
|
|
|
|
while (downloadCount >= maxDownload)
|
|
await Task.Delay(1000);
|
|
|
|
downloadCount++;
|
|
currentStrike++;
|
|
CreateNotification(queue[position].name);
|
|
|
|
try
|
|
{
|
|
YoutubeClient client = new YoutubeClient();
|
|
Video video = await client.GetVideoAsync(queue[position].videoID);
|
|
MediaStreamInfoSet mediaStreamInfo = await client.GetVideoMediaStreamInfosAsync(queue[position].videoID);
|
|
AudioStreamInfo streamInfo = mediaStreamInfo.Audio.Where(x => x.Container == Container.Mp4).OrderBy(s => s.Bitrate).Last();
|
|
|
|
queue[position].State = DownloadState.Downloading;
|
|
UpdateList(position);
|
|
string title = video.Title;
|
|
foreach(char c in Path.GetInvalidFileNameChars()) //Make the title a valid filename (remove /, \, : etc).
|
|
{
|
|
title = title.Replace(c, ' ');
|
|
}
|
|
|
|
string fileExtension = "m4a"; //audio only extension containing aac (audio codex of the mp4)
|
|
|
|
string outpath = path;
|
|
|
|
if (queue[position].playlist != null)
|
|
{
|
|
outpath = Path.Combine(path, queue[position].playlist);
|
|
Directory.CreateDirectory(outpath);
|
|
}
|
|
|
|
string filePath = Path.Combine(outpath, title + "." + fileExtension);
|
|
|
|
if (File.Exists(filePath))
|
|
{
|
|
int i = 1;
|
|
string defaultPath = filePath;
|
|
do
|
|
{
|
|
filePath = Path.Combine(outpath, title + " (" + i + ")." + fileExtension);
|
|
i++;
|
|
}
|
|
while (File.Exists(filePath));
|
|
}
|
|
|
|
MediaStream input = await client.GetMediaStreamAsync(streamInfo);
|
|
|
|
queue[position].path = filePath;
|
|
files.Add(filePath);
|
|
FileStream output = File.Create(filePath);
|
|
|
|
byte[] buffer = new byte[4096];
|
|
int byteReaded = 0;
|
|
while (byteReaded < input.Length)
|
|
{
|
|
int read = await input.ReadAsync(buffer, 0, 4096);
|
|
await output.WriteAsync(buffer, 0, read);
|
|
byteReaded += read;
|
|
UpdateProgressBar(position, byteReaded / input.Length);
|
|
}
|
|
output.Dispose();
|
|
|
|
queue[position].State = DownloadState.MetaData;
|
|
UpdateList(position);
|
|
if (queue.Count == 1)
|
|
SetNotificationProgress(100, true);
|
|
|
|
SetMetaData(position, filePath, video.Title, video.Author, new string[] { video.Thumbnails.MaxResUrl, video.Thumbnails.StandardResUrl, video.Thumbnails.HighResUrl }, queue[position].videoID, queue[position].playlist);
|
|
files.Remove(filePath);
|
|
downloadCount--;
|
|
|
|
if (queue.Count != 0)
|
|
DownloadAudio(queue.FindIndex(x => x.State == DownloadState.None), path);
|
|
|
|
Playlist.instance?.CheckForSync();
|
|
}
|
|
catch (System.Net.Http.HttpRequestException)
|
|
{
|
|
MainActivity.instance.Timout();
|
|
Cancel();
|
|
}
|
|
catch (DirectoryNotFoundException)
|
|
{
|
|
Handler handler = new Handler(Looper.MainLooper);
|
|
handler.Post(() => { Toast.MakeText(Application.Context, Resource.String.download_path_error, ToastLength.Long).Show(); });
|
|
Cancel();
|
|
}
|
|
catch
|
|
{
|
|
MainActivity.instance.UnknowError();
|
|
Cancel();
|
|
}
|
|
}
|
|
|
|
private async void SetMetaData(int position, string filePath, string title, string artist, string[] thumbnails, string youtubeID, string playlist)
|
|
{
|
|
await Task.Run(async () =>
|
|
{
|
|
Stream stream = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite);
|
|
var meta = TagLib.File.Create(new StreamFileAbstraction(filePath, stream, stream));
|
|
|
|
meta.Tag.Title = title;
|
|
meta.Tag.Performers = new string[] { artist };
|
|
meta.Tag.Album = title + " - " + artist;
|
|
meta.Tag.Comment = youtubeID;
|
|
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
bool? canContinue = false;
|
|
WebClient webClient = new WebClient();
|
|
webClient.DownloadDataCompleted += (sender, e) =>
|
|
{
|
|
System.Console.WriteLine("&Error with thumb " + i + ": " + e.Error);
|
|
if (e.Error == null)
|
|
{
|
|
IPicture[] pictures = new IPicture[1];
|
|
Bitmap bitmap = Picasso.With(Application.Context).Load(thumbnails[i]).Transform(new RemoveBlackBorder(true)).Get();
|
|
byte[] data;
|
|
using (var MemoryStream = new MemoryStream())
|
|
{
|
|
bitmap.Compress(Bitmap.CompressFormat.Png, 0, MemoryStream);
|
|
data = MemoryStream.ToArray();
|
|
}
|
|
bitmap.Recycle();
|
|
pictures[0] = new Picture(data);
|
|
meta.Tag.Pictures = pictures;
|
|
canContinue = null;
|
|
}
|
|
else
|
|
canContinue = true;
|
|
};
|
|
try
|
|
{
|
|
await webClient.DownloadDataTaskAsync(new System.Uri(thumbnails[i]));
|
|
}
|
|
catch { } //catch 404 errors
|
|
|
|
while (canContinue == false)
|
|
await Task.Delay(10);
|
|
|
|
if (canContinue == null)
|
|
{
|
|
meta.Save();
|
|
stream.Dispose();
|
|
return;
|
|
}
|
|
}
|
|
|
|
meta.Save();
|
|
stream.Dispose();
|
|
});
|
|
|
|
MediaScannerConnection.ScanFile(this, new string[] { filePath }, null, this);
|
|
|
|
queue[position].State = DownloadState.Completed;
|
|
UpdateList(position);
|
|
|
|
if (!queue.Exists(x => x.State == DownloadState.None || x.State == DownloadState.Downloading || x.State == DownloadState.Initialization || x.State == DownloadState.MetaData))
|
|
{
|
|
StopForeground(true);
|
|
DownloadQueue.instance?.Finish();
|
|
queue.Clear();
|
|
}
|
|
}
|
|
|
|
public void OnScanCompleted(string path, Uri uri)
|
|
{
|
|
Android.Util.Log.Debug("MusisApp", "Scan Completed with path = " + path + " and uri = " + uri.ToString());
|
|
//long id = long.Parse(uri.ToString().Substring(uri.ToString().IndexOf("audio/media/") + 12, uri.ToString().Length - uri.ToString().IndexOf("audio/media/") - 12));
|
|
string playlist = path.Substring(downloadPath.Length + 1);
|
|
|
|
if (playlist.IndexOf('/') != -1)
|
|
{
|
|
playlist = playlist.Substring(0, playlist.IndexOf('/'));
|
|
Handler handler = new Handler(MainActivity.instance.MainLooper);
|
|
handler.Post(() =>
|
|
{
|
|
Browse.AddToPlaylist(Browse.GetSong(path), playlist, -1, true);
|
|
});
|
|
}
|
|
}
|
|
|
|
public async void SyncWithPlaylist(string playlistName, bool keepDeleted)
|
|
{
|
|
Playlist.instance?.StartSyncing(playlistName);
|
|
long LocalID = Browse.GetPlaylistID(playlistName);
|
|
SyncWithPlaylist(LocalID, keepDeleted);
|
|
|
|
await Task.Run(() =>
|
|
{
|
|
SQLiteConnection db = new SQLiteConnection(Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal), "SyncedPlaylists.sqlite"));
|
|
db.CreateTable<PlaylistItem>();
|
|
db.InsertOrReplace(new PlaylistItem(playlistName, LocalID, null));
|
|
});
|
|
|
|
await Task.Delay(1000);
|
|
Playlist.instance?.CheckForSync();
|
|
}
|
|
|
|
public void SyncWithPlaylist(long LocalID, bool keepDeleted)
|
|
{
|
|
if (LocalID != -1)
|
|
{
|
|
Uri musicUri = Playlists.Members.GetContentUri("external", LocalID);
|
|
|
|
CursorLoader cursorLoader = new CursorLoader(Application.Context, musicUri, null, null, null, null);
|
|
ICursor musicCursor = (ICursor)cursorLoader.LoadInBackground();
|
|
|
|
List<string> paths = new List<string>();
|
|
List<long> localIDs = new List<long>();
|
|
List<string> videoIDs = new List<string>();
|
|
|
|
if (musicCursor != null && musicCursor.MoveToFirst())
|
|
{
|
|
int songID = musicCursor.GetColumnIndex(MediaStore.Audio.Media.InterfaceConsts.Id);
|
|
int pathID = musicCursor.GetColumnIndex(Media.InterfaceConsts.Data);
|
|
do
|
|
{
|
|
string path = musicCursor.GetString(pathID);
|
|
long id = musicCursor.GetLong(songID);
|
|
paths.Add(path);
|
|
localIDs.Add(id);
|
|
videoIDs.Add(Browse.GetYtID(path));
|
|
}
|
|
while (musicCursor.MoveToNext());
|
|
musicCursor.Close();
|
|
|
|
|
|
for (int i = 0; i < queue.Count; i++)
|
|
{
|
|
if (videoIDs.Contains(queue[i].videoID))
|
|
{
|
|
//Video is already downloaded:
|
|
if (queue[i].State == DownloadState.None)
|
|
queue[i].State = DownloadState.UpToDate;
|
|
|
|
currentStrike++;
|
|
int Index = videoIDs.FindIndex(x => x == queue[i].videoID);
|
|
paths.RemoveAt(Index);
|
|
localIDs.RemoveAt(Index);
|
|
videoIDs.RemoveAt(Index);
|
|
}
|
|
}
|
|
|
|
|
|
for (int i = 0; i < paths.Count; i++)
|
|
{
|
|
//Video has been removed from the playlist but still exist on local storage
|
|
ContentResolver resolver = Application.ContentResolver;
|
|
Uri uri = Playlists.Members.GetContentUri("external", LocalID);
|
|
resolver.Delete(uri, Playlists.Members.Id + "=?", new string[] { localIDs[i].ToString() });
|
|
|
|
if(!keepDeleted)
|
|
File.Delete(paths[i]);
|
|
}
|
|
}
|
|
}
|
|
Playlist.instance?.CheckForSync();
|
|
}
|
|
|
|
void Cancel()
|
|
{
|
|
cancellation.Cancel(true);
|
|
queue = new List<DownloadFile>();
|
|
SetCancelNotification();
|
|
DownloadQueue.instance?.Finish();
|
|
CleanDownload();
|
|
}
|
|
|
|
void CleanDownload()
|
|
{
|
|
Task.Run(() =>
|
|
{
|
|
foreach (string filePath in files)
|
|
{
|
|
if (File.Exists(filePath))
|
|
File.Delete(filePath);
|
|
}
|
|
});
|
|
|
|
downloadCount = 0;
|
|
currentStrike = 0;
|
|
StopForeground(true);
|
|
cancellation = new CancellationTokenSource();
|
|
Playlist.instance?.SyncCanceled();
|
|
}
|
|
|
|
void UpdateList(int position)
|
|
{
|
|
DownloadQueue.instance?.RunOnUiThread(() => { DownloadQueue.instance?.ListView.GetAdapter().NotifyItemChanged(position); });
|
|
}
|
|
|
|
void UpdateProgressBar(int position, long progress)
|
|
{
|
|
queue[position].progress = (int)(progress * 100);
|
|
DownloadQueue.instance?.RunOnUiThread(() =>
|
|
{
|
|
LinearLayoutManager LayoutManager = (LinearLayoutManager)DownloadQueue.instance?.ListView?.GetLayoutManager();
|
|
if (LayoutManager != null && position >= LayoutManager.FindFirstVisibleItemPosition() && position <= LayoutManager.FindLastVisibleItemPosition())
|
|
{
|
|
View view = DownloadQueue.instance.ListView.GetChildAt(position - LayoutManager.FindFirstVisibleItemPosition());
|
|
DownloadHolder holder = (DownloadHolder)DownloadQueue.instance.ListView.GetChildViewHolder(view);
|
|
holder.Progress.Progress = (int)(progress * 100);
|
|
}
|
|
});
|
|
if (queue.Count == 1)
|
|
SetNotificationProgress((int)(progress * 100));
|
|
}
|
|
|
|
void CreateNotification(string title)
|
|
{
|
|
Intent intent = new Intent(MainActivity.instance, typeof(DownloadQueue));
|
|
PendingIntent queueIntent = PendingIntent.GetActivity(Application.Context, 471, intent, PendingIntentFlags.UpdateCurrent);
|
|
|
|
intent = new Intent(MainActivity.instance, typeof(Downloader));
|
|
intent.SetAction("Cancel");
|
|
PendingIntent cancelIntent = PendingIntent.GetService(Application.Context, 471, intent, PendingIntentFlags.OneShot);
|
|
|
|
notification = new NotificationCompat.Builder(Application.Context, "MusicApp.Channel")
|
|
.SetVisibility(NotificationCompat.VisibilityPublic)
|
|
.SetSmallIcon(Resource.Drawable.MusicIcon)
|
|
.SetContentTitle("Downloading: ")
|
|
.SetContentText(downloadCount > 1 ? "Tap for more details" : title)
|
|
.SetContentIntent(queueIntent)
|
|
.AddAction(Resource.Drawable.Cancel, "Cancel", cancelIntent);
|
|
|
|
if(queue.Count > 1)
|
|
{
|
|
notification.SetSubText(currentStrike + "/" + queue.Count);
|
|
notification.SetProgress(queue.Count, currentStrike, false);
|
|
}
|
|
else
|
|
{
|
|
notification.SetProgress(100, 0, true);
|
|
}
|
|
|
|
StartForeground(notificationID, notification.Build());
|
|
}
|
|
|
|
void SetNotificationProgress(int progress, bool indeterminate = false)
|
|
{
|
|
notification.SetProgress(100, progress, indeterminate);
|
|
}
|
|
|
|
void SetCancelNotification()
|
|
{
|
|
notification.SetContentTitle("Cancelling...")
|
|
.SetSubText((string)null);
|
|
|
|
StartForeground(notificationID, notification.Build());
|
|
}
|
|
}
|
|
|
|
public enum DownloadState
|
|
{
|
|
Initialization,
|
|
Downloading,
|
|
MetaData,
|
|
Completed,
|
|
Canceled,
|
|
UpToDate,
|
|
None
|
|
}
|
|
}
|
|
|