Merge branch 'master' of code.manuelcortez.net:manuelcortez/music-dl

This commit is contained in:
Manuel Cortez 2020-07-17 21:56:06 -05:00
commit 693a9f474e
9 changed files with 140 additions and 62 deletions

View File

@ -2,11 +2,13 @@
## Version 0.7
* changes in Tidal:
* Tidal:
* Added a new search mode for the service to retrieve the top rated tracks of a provided artist. The sintax is top://artist.
* In the settings dialog, you can control wether Albums, compilations and singles will be added when searching by artist (by using artist://...).
* When searching by artists, results that belong to an album will be numbered.
* Downloads will be tagged with title, album, artist and track number provided by tidal.
* changes in Youtube:
* It is possible to download an original version in high and low quality. Before, those versions were encoded to mp3 from an m4a file. Now the M4a file can be retrieved by ticking the checkbox in the tidal settings page.
* YouTube:
* Fixed search algorithm for Youtube videos.
* Updated Youtube-Dl to version 2020.6.16.1
* re-added VK module. By default, this module searches up to 50 results but you can increase it up to 200 if needed from the services settings.

View File

@ -8,10 +8,12 @@ authorEmail = "manuel@manuelcortez.net"
copyright = "Copyright (C) 2019-2020, Manuel Cortez"
description = name+_(u" Is an application that will allow you to download music from popular sites such as youtube, zaycev.net.")
url = "https://manuelcortez.net/music_dl"
update_url = "https://manuelcortez.net/music_dl/update"
# The short name will be used for detecting translation files. See languageHandler for more details.
short_name = "musicdl"
translators = [_(u"Manuel Cortez (Spanish)")]
bts_name = "music_dl"
bts_access_token = "fe3j2ijirvevv9"
bts_url = "https://issues.manuelcortez.net"
bts_url = "https://issues.manuelcortez.net"
update_stable_url = "https://manuelcortez.net/static/files/music_dl/update/stable.json"
update_next_url = "https://manuelcortez.net/static/files/music_dl/update/alpha.json"
update_next_version=None

View File

@ -66,6 +66,10 @@ class interface(base.baseInterface):
if text == "" or text == None:
raise ValueError("Text must be passed and should not be blank.")
log.debug("Retrieving data from Tidal...")
# Check for top:// protocol.
if text.startswith("top://"):
text = text.replace("top://", "")
return self.search_for_top(text)
fieldtypes = ["artist", "album", "playlist"]
field = "track"
for i in fieldtypes:
@ -114,6 +118,25 @@ class interface(base.baseInterface):
self.results.append(s)
log.debug("{0} results found.".format(len(self.results)))
def search_for_top(self, artist):
search_response = self.session.search(value=artist, field="artist")
self.results = []
artist = search_response.artists[0].id
results = self.session.get_artist_top_tracks(artist)
for search_result in results:
s = base.song(self)
s.title = search_result.name
s.artist = search_result.artist.name
s.duration = seconds_to_string(search_result.duration)
s.url = search_result.id
s.tracknumber = str(search_result.track_num)
s.album = search_result.album.name
if search_result.album.num_tracks == None:
s.single = True
s.info = search_result
self.results.append(s)
log.debug("{0} results found.".format(len(self.results)))
def format_number(self, number):
if number < 10:
return "0%d" % (number)

View File

@ -0,0 +1,12 @@
import glob
import os.path
import platform
def find_datafiles():
system = platform.system()
if system == 'Windows':
file_ext = '*.exe'
else:
file_ext = '*.sh'
path = os.path.abspath(os.path.join(__path__[0], 'bootstrappers', file_ext))
return [('', glob.glob(path))]

View File

@ -1,31 +1,31 @@
from logging import getLogger
logger = getLogger('update')
import sys
import contextlib
import io
import os
import platform
import requests
import tempfile
import widgetUtils
import webbrowser
try:
import czipfile as zipfile
except ImportError:
import zipfile
import paths
from platform_utils import paths
def perform_update(endpoint, current_version, app_name='', password=None, update_available_callback=None, progress_callback=None, update_complete_callback=None):
def perform_update(endpoint, current_version, update_type="stable", app_name='', password=None, update_available_callback=None, progress_callback=None, update_complete_callback=None):
requests_session = create_requests_session(app_name=app_name, version=current_version)
available_update = find_update(endpoint, requests_session=requests_session)
if not available_update:
logger.debug("No update available")
return False
available_version = float(available_update['current_version'])
if not float(available_version) > float(current_version) or platform.system()+platform.architecture()[0][:2] not in available_update['downloads']:
logger.debug("No update for this architecture")
available_version, available_description, update_url = find_version_data(update_type, current_version, available_update)
if available_version == False:
return False
available_description = available_update.get('description', None)
update_url = available_update ['downloads'][platform.system()+platform.architecture()[0][:2]]
logger.info("A new update is available. Version %s" % available_version)
if callable(update_available_callback) and not update_available_callback(version=available_version, description=available_description): #update_available_callback should return a falsy value to stop the process
logger.info("User canceled update.")
@ -55,6 +55,23 @@ def find_update(endpoint, requests_session):
content = response.json()
return content
def find_version_data(update_type, current_version, available_update):
if update_type == "stable":
available_version = float(available_update['current_version'])
if not float(available_version) > float(current_version) or platform.system()+platform.architecture()[0][:2] not in available_update['downloads']:
logger.debug("No update for this architecture")
return (False, False, False)
available_description = available_update.get('description', None)
update_url = available_update ['downloads'][platform.system()+platform.architecture()[0][:2]]
return (available_version, available_description, update_url)
else: # Unstable versions, based in commits instead of version numbers.
available_version = available_update["current_version"]
if available_version == current_version:
return (False, False, False)
available_description = available_update["description"]
update_url = available_update ['downloads'][platform.system()+platform.architecture()[0][:2]]
return (available_version, available_description, update_url)
def download_update(update_url, update_destination, requests_session, progress_callback=None, chunk_size=io.DEFAULT_BUFFER_SIZE):
total_downloaded = total_size = 0
with io.open(update_destination, 'w+b') as outfile:

View File

@ -1,14 +1,24 @@
# -*- coding: utf-8 -*-
import sys
import application
import platform
import logging
from requests.exceptions import ConnectionError
from .import update
from . import update
from .wxUpdater import *
logger = logging.getLogger("updater")
def do_update(endpoint=application.update_url):
try:
return update.perform_update(endpoint=endpoint, current_version=application.version, app_name=application.name, update_available_callback=available_update_dialog, progress_callback=progress_callback, update_complete_callback=update_finished)
except ConnectionError:
logger.exception("Update failed.")
def do_update(update_type="alpha"):
# Updates cannot be performed in the source code version.
if hasattr(sys, "frozen") == False:
return
if update_type == "stable":
endpoint = application.update_stable_url
version = application.version
else:
endpoint = application.update_next_url
version = application.update_next_version
try:
return update.perform_update(endpoint=endpoint, current_version=version, app_name=application.name, update_type=update_type, update_available_callback=available_update_dialog, progress_callback=progress_callback, update_complete_callback=update_finished)
except ConnectionError:
logger.exception("Update failed.")

View File

@ -1,42 +1,43 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
def convert_bytes(n):
K, M, G, T, P = 1 << 10, 1 << 20, 1 << 30, 1 << 40, 1 << 50
if n >= P:
return '%.2fPb' % (float(n) / T)
elif n >= T:
return '%.2fTb' % (float(n) / T)
elif n >= G:
return '%.2fGb' % (float(n) / G)
elif n >= M:
return '%.2fMb' % (float(n) / M)
elif n >= K:
return '%.2fKb' % (float(n) / K)
else:
return '%d' % n
K, M, G, T, P = 1 << 10, 1 << 20, 1 << 30, 1 << 40, 1 << 50
if n >= P:
return '%.2fPb' % (float(n) / T)
elif n >= T:
return '%.2fTb' % (float(n) / T)
elif n >= G:
return '%.2fGb' % (float(n) / G)
elif n >= M:
return '%.2fMb' % (float(n) / M)
elif n >= K:
return '%.2fKb' % (float(n) / K)
else:
return '%d' % n
def seconds_to_string(seconds, precision=0):
day = seconds // 86400
hour = seconds // 3600
min = (seconds // 60) % 60
sec = seconds - (hour * 3600) - (min * 60)
sec_spec = "." + str(precision) + "f"
sec_string = sec.__format__(sec_spec)
string = ""
if day == 1:
string += _(u"%d day, ") % day
elif day >= 2:
string += _(u"%d days, ") % day
if (hour == 1):
string += _(u"%d hour, ") % hour
elif (hour >= 2):
string += _("%d hours, ") % hour
if (min == 1):
string += _(u"%d minute, ") % min
elif (min >= 2):
string += _(u"%d minutes, ") % min
if sec >= 0 and sec <= 2:
string += _(u"%s second") % sec_string
else:
string += _(u"%s seconds") % sec_string
return string
day = seconds // 86400
hour = seconds // 3600
min = (seconds // 60) % 60
sec = seconds - (hour * 3600) - (min * 60)
sec_spec = "." + str(precision) + "f"
sec_string = sec.__format__(sec_spec)
string = ""
if day == 1:
string += _("%d day, ") % day
elif day >= 2:
string += _("%d days, ") % day
if (hour == 1):
string += _("%d hour, ") % hour
elif (hour >= 2):
string += _("%d hours, ") % hour
if (min == 1):
string += _("%d minute, ") % min
elif (min >= 2):
string += _("%d minutes, ") % min
if sec >= 0 and sec <= 2:
string += _("%s second") % sec_string
else:
string += _("%s seconds") % sec_string
return string

View File

@ -1,22 +1,26 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals # at top of module
from __future__ import division
from __future__ import unicode_literals
import wx
import application
from .import utils
from . import utils
progress_dialog = None
def available_update_dialog(version, description):
dialog = wx.MessageDialog(None, _(u"There's a new %s version available. Would you like to download it now?\n\n %s version: %s\n\nChanges:\n%s") % (application.name, application.name, version, description), _(u"New version for %s") % application.name, style=wx.YES|wx.NO|wx.ICON_WARNING)
dialog = wx.MessageDialog(None, _("There's a new {app_name} version available. Would you like to download it now?\n\n {app_name} version: {app_version}\n\nChanges:\n{changes}").format(app_name=application.name, app_version=version, changes=description), _("New version for %s") % application.name, style=wx.YES|wx.NO|wx.ICON_WARNING)
if dialog.ShowModal() == wx.ID_YES:
return True
else:
return False
def create_progress_dialog():
return wx.ProgressDialog(_(u"Download in Progress"), _(u"Downloading the new version..."), parent=None, maximum=100)
return wx.ProgressDialog(_("Download in Progress"), _("Downloading the new version..."), parent=None, maximum=100)
def progress_callback(total_downloaded, total_size):
wx.CallAfter(_progress_callback, total_downloaded, total_size)
def _progress_callback(total_downloaded, total_size):
global progress_dialog
if progress_dialog == None:
progress_dialog = create_progress_dialog()
@ -24,7 +28,7 @@ def progress_callback(total_downloaded, total_size):
if total_downloaded == total_size:
progress_dialog.Destroy()
else:
progress_dialog.Update((total_downloaded*100)/total_size, _(u"Updating... %s of %s") % (str(utils.convert_bytes(total_downloaded)), str(utils.convert_bytes(total_size))))
progress_dialog.Update((total_downloaded*100)/total_size, _("Updating... {total_transferred} of {total_size}").format(total_transferred=utils.convert_bytes(total_downloaded), total_size=utils.convert_bytes(total_size)))
def update_finished():
ms = wx.MessageDialog(None, _(u"The update has been downloaded and installed successfully. Press OK to continue."), _(u"Done!")).ShowModal()
return wx.MessageDialog(None, _("The update has been downloaded and installed successfully. Press OK to continue."), _("Done!")).ShowModal()

View File

@ -92,7 +92,14 @@ def apply_metadata(local_filename, metadata):
from mutagen.easyid3 import EasyID3 as metadataeditor
elif local_filename.endswith(".flac"):
from mutagen.flac import FLAC as metadataeditor
elif local_filename.endswith(".m4a"):
from mutagen.mp4 import MP4 as metadataeditor
audio = metadataeditor(local_filename)
for k in metadata.keys():
audio[k] = metadata[k]
if local_filename.endswith(".m4a") == False:
for k in metadata.keys():
audio[k] = metadata[k]
else:
audio["\xa9nam"] = metadata["title"]
audio["\xa9alb"] = metadata["album"]
audio["\xa9ART"] = metadata["artist"]
audio.save()