2020-08-15 19:34:10 +00:00
#!/usr/bin/env python3
import os . path
import re
from requests import get
2020-09-07 10:09:46 +00:00
from requests import exceptions as request_exception
2020-08-15 19:34:10 +00:00
from concurrent . futures import ThreadPoolExecutor
from os import makedirs , remove , system as execute
from tempfile import gettempdir
from time import sleep
2020-09-12 11:08:28 +00:00
from deemix . app . queueitem import QISingle , QICollection
2020-08-15 21:03:05 +00:00
from deemix . app . track import Track
2020-08-15 19:34:10 +00:00
from deemix . utils . misc import changeCase
from deemix . utils . pathtemplates import generateFilename , generateFilepath , settingsRegexAlbum , settingsRegexArtist , settingsRegexPlaylistFile
from deemix . api . deezer import USER_AGENT_HEADER
from deemix . utils . taggers import tagID3 , tagFLAC
from Cryptodome . Cipher import Blowfish
from mutagen . flac import FLACNoHeaderError
import logging
logging . basicConfig ( level = logging . INFO )
logger = logging . getLogger ( ' deemix ' )
TEMPDIR = os . path . join ( gettempdir ( ) , ' deemix-imgs ' )
if not os . path . isdir ( TEMPDIR ) :
makedirs ( TEMPDIR )
extensions = {
9 : ' .flac ' ,
0 : ' .mp3 ' ,
3 : ' .mp3 ' ,
1 : ' .mp3 ' ,
8 : ' .mp3 ' ,
15 : ' .mp4 ' ,
14 : ' .mp4 ' ,
13 : ' .mp4 '
}
errorMessages = {
' notOnDeezer ' : " Track not available on Deezer! " ,
' notEncoded ' : " Track not yet encoded! " ,
' notEncodedNoAlternative ' : " Track not yet encoded and no alternative found! " ,
' wrongBitrate ' : " Track not found at desired bitrate. " ,
' wrongBitrateNoAlternative ' : " Track not found at desired bitrate and no alternative found! " ,
' no360RA ' : " Track is not available in Reality Audio 360. " ,
2020-08-15 21:03:05 +00:00
' notAvailable ' : " Track not available on deezer ' s servers! " ,
' notAvailableNoAlternative ' : " Track not available on deezer ' s servers and no alternative found! "
2020-08-15 19:34:10 +00:00
}
def downloadImage ( url , path , overwrite = " n " ) :
if not os . path . isfile ( path ) or overwrite in [ ' y ' , ' t ' , ' b ' ] :
try :
image = get ( url , headers = { ' User-Agent ' : USER_AGENT_HEADER } , timeout = 30 )
image . raise_for_status ( )
with open ( path , ' wb ' ) as f :
f . write ( image . content )
return path
2020-09-07 10:09:46 +00:00
except request_exception . HTTPError :
2020-08-15 19:34:10 +00:00
if ' cdns-images.dzcdn.net ' in url :
urlBase = url [ : url . rfind ( " / " ) + 1 ]
pictureUrl = url [ len ( urlBase ) : ]
pictureSize = int ( pictureUrl [ : pictureUrl . find ( " x " ) ] )
if pictureSize > 1200 :
logger . warn ( " Couldn ' t download " + str ( pictureSize ) + " x " + str ( pictureSize ) + " image, falling back to 1200x1200 " )
sleep ( 1 )
return downloadImage ( urlBase + pictureUrl . replace ( str ( pictureSize ) + " x " + str ( pictureSize ) , ' 1200x1200 ' ) , path , overwrite )
2020-09-10 09:43:32 +00:00
logger . error ( " Image not found: " + url )
except ( request_exception . ConnectionError , request_exception . ChunkedEncodingError ) as e :
2020-09-10 10:43:53 +00:00
logger . error ( " Couldn ' t download Image, retrying in 5 seconds...: " + url + " \n " )
2020-09-10 09:43:32 +00:00
sleep ( 5 )
2020-08-15 19:34:10 +00:00
return downloadImage ( url , path , overwrite )
2020-09-10 09:43:32 +00:00
except Exception as e :
logger . exception ( f " Error while downloading an image, you should report this to the developers: { str ( e ) } " )
2020-08-21 12:40:36 +00:00
if os . path . isfile ( path ) : remove ( path )
2020-08-15 19:34:10 +00:00
return None
else :
return path
def formatDate ( date , template ) :
elements = {
' year ' : [ ' YYYY ' , ' YY ' , ' Y ' ] ,
' month ' : [ ' MM ' , ' M ' ] ,
' day ' : [ ' DD ' , ' D ' ]
}
for element , placeholders in elements . items ( ) :
for placeholder in placeholders :
if placeholder in template :
template = template . replace ( placeholder , str ( date [ element ] ) )
return template
class DownloadJob :
2020-09-12 11:08:28 +00:00
def __init__ ( self , dz , queueItem , interface = None ) :
2020-08-15 19:34:10 +00:00
self . dz = dz
self . interface = interface
2020-08-15 21:03:05 +00:00
self . queueItem = queueItem
2020-08-15 19:34:10 +00:00
self . settings = queueItem . settings
self . bitrate = queueItem . bitrate
self . downloadPercentage = 0
self . lastPercentage = 0
2020-08-17 15:46:21 +00:00
self . extrasPath = None
2020-08-15 21:36:32 +00:00
self . playlistPath = None
self . playlistURLs = [ ]
2020-08-15 19:34:10 +00:00
def start ( self ) :
2020-08-19 15:16:39 +00:00
if not self . queueItem . cancel :
if isinstance ( self . queueItem , QISingle ) :
result = self . downloadWrapper ( self . queueItem . single )
if result :
self . singleAfterDownload ( result )
elif isinstance ( self . queueItem , QICollection ) :
tracks = [ None ] * len ( self . queueItem . collection )
with ThreadPoolExecutor ( self . settings [ ' queueConcurrency ' ] ) as executor :
for pos , track in enumerate ( self . queueItem . collection , start = 0 ) :
tracks [ pos ] = executor . submit ( self . downloadWrapper , track )
self . collectionAfterDownload ( tracks )
2020-08-15 19:34:10 +00:00
if self . interface :
if self . queueItem . cancel :
self . interface . send ( ' currentItemCancelled ' , self . queueItem . uuid )
self . interface . send ( " removedFromQueue " , self . queueItem . uuid )
else :
self . interface . send ( " finishDownload " , self . queueItem . uuid )
2020-08-15 21:36:32 +00:00
return self . extrasPath
def singleAfterDownload ( self , result ) :
2020-08-17 15:46:21 +00:00
if not self . extrasPath :
self . extrasPath = self . settings [ ' downloadLocation ' ]
2020-08-15 21:36:32 +00:00
# Save Album Cover
if self . settings [ ' saveArtwork ' ] and ' albumPath ' in result :
for image in result [ ' albumURLs ' ] :
downloadImage ( image [ ' url ' ] , f " { result [ ' albumPath ' ] } . { image [ ' ext ' ] } " , self . settings [ ' overwriteFile ' ] )
# Save Artist Artwork
if self . settings [ ' saveArtworkArtist ' ] and ' artistPath ' in result :
for image in result [ ' artistURLs ' ] :
downloadImage ( image [ ' url ' ] , f " { result [ ' artistPath ' ] } . { image [ ' ext ' ] } " , self . settings [ ' overwriteFile ' ] )
# Create searched logfile
if self . settings [ ' logSearched ' ] and ' searched ' in result :
with open ( os . path . join ( self . extrasPath , ' searched.txt ' ) , ' wb+ ' ) as f :
orig = f . read ( ) . decode ( ' utf-8 ' )
if not result [ ' searched ' ] in orig :
if orig != " " :
orig + = " \r \n "
orig + = result [ ' searched ' ] + " \r \n "
f . write ( orig . encode ( ' utf-8 ' ) )
# Execute command after download
if self . settings [ ' executeCommand ' ] != " " :
execute ( self . settings [ ' executeCommand ' ] . replace ( " %f older % " , self . extrasPath ) . replace ( " %f ilename % " , result [ ' filename ' ] ) )
def collectionAfterDownload ( self , tracks ) :
2020-08-17 15:46:21 +00:00
if not self . extrasPath :
self . extrasPath = self . settings [ ' downloadLocation ' ]
2020-08-15 21:36:32 +00:00
playlist = [ None ] * len ( tracks )
errors = " "
searched = " "
for index in range ( len ( tracks ) ) :
result = tracks [ index ] . result ( )
# Check if queue is cancelled
if not result :
return None
# Log errors to file
if ' error ' in result :
if not ' data ' in result [ ' error ' ] :
2020-08-18 13:13:32 +00:00
result [ ' error ' ] [ ' data ' ] = { ' id ' : " 0 " , ' title ' : ' Unknown ' , ' artist ' : ' Unknown ' }
2020-08-15 21:36:32 +00:00
errors + = f " { result [ ' error ' ] [ ' data ' ] [ ' id ' ] } | { result [ ' error ' ] [ ' data ' ] [ ' artist ' ] } - { result [ ' error ' ] [ ' data ' ] [ ' title ' ] } | { result [ ' error ' ] [ ' message ' ] } \r \n "
# Log searched to file
if ' searched ' in result :
searched + = result [ ' searched ' ] + " \r \n "
# Save Album Cover
if self . settings [ ' saveArtwork ' ] and ' albumPath ' in result :
for image in result [ ' albumURLs ' ] :
downloadImage ( image [ ' url ' ] , f " { result [ ' albumPath ' ] } . { image [ ' ext ' ] } " , self . settings [ ' overwriteFile ' ] )
# Save Artist Artwork
if self . settings [ ' saveArtworkArtist ' ] and ' artistPath ' in result :
for image in result [ ' artistURLs ' ] :
downloadImage ( image [ ' url ' ] , f " { result [ ' artistPath ' ] } . { image [ ' ext ' ] } " , self . settings [ ' overwriteFile ' ] )
# Save filename for playlist file
playlist [ index ] = " "
if ' filename ' in result :
playlist [ index ] = result [ ' filename ' ]
# Create errors logfile
if self . settings [ ' logErrors ' ] and errors != " " :
with open ( os . path . join ( self . extrasPath , ' errors.txt ' ) , ' wb ' ) as f :
f . write ( errors . encode ( ' utf-8 ' ) )
# Create searched logfile
if self . settings [ ' logSearched ' ] and searched != " " :
with open ( os . path . join ( self . extrasPath , ' searched.txt ' ) , ' wb ' ) as f :
f . write ( searched . encode ( ' utf-8 ' ) )
# Save Playlist Artwork
if self . settings [ ' saveArtwork ' ] and self . playlistPath and not self . settings [ ' tags ' ] [ ' savePlaylistAsCompilation ' ] :
for image in self . playlistURLs :
downloadImage ( image [ ' url ' ] , os . path . join ( self . extrasPath , self . playlistPath ) + f " . { image [ ' ext ' ] } " , self . settings [ ' overwriteFile ' ] )
# Create M3U8 File
if self . settings [ ' createM3U8File ' ] :
2020-08-16 17:09:32 +00:00
filename = settingsRegexPlaylistFile ( self . settings [ ' playlistFilenameTemplate ' ] , self . queueItem , self . settings ) or " playlist "
2020-08-15 21:36:32 +00:00
with open ( os . path . join ( self . extrasPath , filename + ' .m3u8 ' ) , ' wb ' ) as f :
for line in playlist :
f . write ( ( line + " \n " ) . encode ( ' utf-8 ' ) )
# Execute command after download
if self . settings [ ' executeCommand ' ] != " " :
execute ( self . settings [ ' executeCommand ' ] . replace ( " %f older % " , self . extrasPath ) )
2020-08-15 19:34:10 +00:00
def download ( self , trackAPI_gw , track = None ) :
result = { }
if self . queueItem . cancel : raise DownloadCancelled
if trackAPI_gw [ ' SNG_ID ' ] == " 0 " :
raise DownloadFailed ( " notOnDeezer " )
# Create Track object
if not track :
logger . info ( f " [ { trackAPI_gw [ ' ART_NAME ' ] } - { trackAPI_gw [ ' SNG_TITLE ' ] } ] Getting the tags " )
track = Track ( self . dz ,
settings = self . settings ,
trackAPI_gw = trackAPI_gw ,
trackAPI = trackAPI_gw [ ' _EXTRA_TRACK ' ] if ' _EXTRA_TRACK ' in trackAPI_gw else None ,
albumAPI = trackAPI_gw [ ' _EXTRA_ALBUM ' ] if ' _EXTRA_ALBUM ' in trackAPI_gw else None
)
if self . queueItem . cancel : raise DownloadCancelled
2020-08-15 21:03:05 +00:00
if track . MD5 == ' ' :
2020-08-15 19:34:10 +00:00
if track . fallbackId != " 0 " :
logger . warn ( f " [ { track . mainArtist [ ' name ' ] } - { track . title } ] Track not yet encoded, using fallback id " )
newTrack = self . dz . get_track_gw ( track . fallbackId )
track . parseEssentialData ( self . dz , newTrack )
return self . download ( trackAPI_gw , track )
elif not track . searched and self . settings [ ' fallbackSearch ' ] :
2020-08-15 21:03:05 +00:00
logger . warn ( f " [ { track . mainArtist [ ' name ' ] } - { track . title } ] Track not yet encoded, searching for alternative " )
searchedId = self . dz . get_track_from_metadata ( track . mainArtist [ ' name ' ] , track . title , track . album [ ' title ' ] )
2020-08-18 13:13:32 +00:00
if searchedId != " 0 " :
2020-08-15 19:34:10 +00:00
newTrack = self . dz . get_track_gw ( searchedId )
track . parseEssentialData ( self . dz , newTrack )
track . searched = True
return self . download ( trackAPI_gw , track )
else :
raise DownloadFailed ( " notEncodedNoAlternative " )
else :
raise DownloadFailed ( " notEncoded " )
selectedFormat = self . getPreferredBitrate ( track )
if selectedFormat == - 100 :
if track . fallbackId != " 0 " :
logger . warn ( f " [ { track . mainArtist [ ' name ' ] } - { track . title } ] Track not found at desired bitrate, using fallback id " )
newTrack = self . dz . get_track_gw ( track . fallbackId )
track . parseEssentialData ( self . dz , newTrack )
return self . download ( trackAPI_gw , track )
elif not track . searched and self . settings [ ' fallbackSearch ' ] :
2020-08-15 21:03:05 +00:00
logger . warn ( f " [ { track . mainArtist [ ' name ' ] } - { track . title } ] Track not found at desired bitrate, searching for alternative " )
searchedId = self . dz . get_track_from_metadata ( track . mainArtist [ ' name ' ] , track . title , track . album [ ' title ' ] )
2020-08-18 13:13:32 +00:00
if searchedId != " 0 " :
2020-08-15 19:34:10 +00:00
newTrack = self . dz . get_track_gw ( searchedId )
track . parseEssentialData ( self . dz , newTrack )
track . searched = True
return self . download ( trackAPI_gw , track )
else :
raise DownloadFailed ( " wrongBitrateNoAlternative " )
else :
raise DownloadFailed ( " wrongBitrate " )
elif selectedFormat == - 200 :
raise DownloadFailed ( " no360RA " )
track . selectedFormat = selectedFormat
if self . settings [ ' tags ' ] [ ' savePlaylistAsCompilation ' ] and track . playlist :
track . trackNumber = track . position
track . discNumber = " 1 "
track . album = { * * track . album , * * track . playlist }
2020-08-21 12:40:36 +00:00
ext = track . playlist [ ' picUrl ' ] [ - 4 : ]
if ext [ 0 ] != " . " :
ext = " .jpg "
track . album [ ' picPath ' ] = os . path . join ( TEMPDIR , f " pl { trackAPI_gw [ ' _EXTRA_PLAYLIST ' ] [ ' id ' ] } _ { self . settings [ ' embeddedArtworkSize ' ] } { ext } " )
2020-08-15 19:34:10 +00:00
else :
if track . album [ ' date ' ] :
track . date = track . album [ ' date ' ]
track . album [ ' picUrl ' ] = " https://e-cdns-images.dzcdn.net/images/cover/ {} / {} x {} - {} " . format (
track . album [ ' pic ' ] ,
self . settings [ ' embeddedArtworkSize ' ] , self . settings [ ' embeddedArtworkSize ' ] ,
2020-08-21 12:40:36 +00:00
' none-100-0-0.png ' if self . settings [ ' embeddedArtworkPNG ' ] else f ' 000000- { self . settings [ " jpegImageQuality " ] } -0-0.jpg '
)
track . album [ ' picPath ' ] = os . path . join ( TEMPDIR , f " alb { track . album [ ' id ' ] } _ { self . settings [ ' embeddedArtworkSize ' ] } { track . album [ ' picUrl ' ] [ - 4 : ] } " )
2020-08-15 19:34:10 +00:00
track . album [ ' bitrate ' ] = selectedFormat
2020-08-15 21:03:05 +00:00
track . dateString = formatDate ( track . date , self . settings [ ' dateFormat ' ] )
track . album [ ' dateString ' ] = formatDate ( track . album [ ' date ' ] , self . settings [ ' dateFormat ' ] )
2020-08-15 19:34:10 +00:00
# Check if user wants the feat in the title
# 0 => do not change
# 1 => remove from title
# 2 => add to title
# 3 => remove from title and album title
if self . settings [ ' featuredToTitle ' ] == " 1 " :
track . title = track . getCleanTitle ( )
elif self . settings [ ' featuredToTitle ' ] == " 2 " :
track . title = track . getFeatTitle ( )
elif self . settings [ ' featuredToTitle ' ] == " 3 " :
track . title = track . getCleanTitle ( )
track . album [ ' title ' ] = track . getCleanAlbumTitle ( )
# Remove (Album Version) from tracks that have that
if self . settings [ ' removeAlbumVersion ' ] :
if " Album Version " in track . title :
track . title = re . sub ( r ' ? \ (Album Version \ ) ' , " " , track . title ) . strip ( )
2020-08-24 11:07:38 +00:00
# Change Title and Artists casing if needed
if self . settings [ ' titleCasing ' ] != " nothing " :
track . title = changeCase ( track . title , self . settings [ ' titleCasing ' ] )
if self . settings [ ' artistCasing ' ] != " nothing " :
track . mainArtist [ ' name ' ] = changeCase ( track . mainArtist [ ' name ' ] , self . settings [ ' artistCasing ' ] )
for i , artist in enumerate ( track . artists ) :
track . artists [ i ] = changeCase ( artist , self . settings [ ' artistCasing ' ] )
for type in track . artist :
for i , artist in enumerate ( track . artist [ type ] ) :
track . artist [ type ] [ i ] = changeCase ( artist , self . settings [ ' artistCasing ' ] )
track . generateMainFeatStrings ( )
2020-08-15 19:34:10 +00:00
# Generate artist tag if needed
if self . settings [ ' tags ' ] [ ' multiArtistSeparator ' ] != " default " :
if self . settings [ ' tags ' ] [ ' multiArtistSeparator ' ] == " andFeat " :
track . artistsString = track . mainArtistsString
2020-09-07 14:13:50 +00:00
if track . featArtistsString and str ( self . settings [ ' featuredToTitle ' ] ) != " 2 " :
2020-08-15 19:34:10 +00:00
track . artistsString + = " " + track . featArtistsString
else :
track . artistsString = self . settings [ ' tags ' ] [ ' multiArtistSeparator ' ] . join ( track . artists )
else :
track . artistsString = " , " . join ( track . artists )
# Generate filename and filepath from metadata
filename = generateFilename ( track , trackAPI_gw , self . settings )
( filepath , artistPath , coverPath , extrasPath ) = generateFilepath ( track , trackAPI_gw , self . settings )
if self . queueItem . cancel : raise DownloadCancelled
# Download and cache coverart
logger . info ( f " [ { track . mainArtist [ ' name ' ] } - { track . title } ] Getting the album cover " )
track . album [ ' picPath ' ] = downloadImage ( track . album [ ' picUrl ' ] , track . album [ ' picPath ' ] )
# Save local album art
if coverPath :
result [ ' albumURLs ' ] = [ ]
for format in self . settings [ ' localArtworkFormat ' ] . split ( " , " ) :
if format in [ " png " , " jpg " ] :
2020-08-21 12:40:36 +00:00
if self . settings [ ' tags ' ] [ ' savePlaylistAsCompilation ' ] and track . playlist :
if track . playlist [ ' pic ' ] :
url = " {} / {} x {} - {} " . format (
track . album [ ' pic ' ] ,
self . settings [ ' localArtworkSize ' ] , self . settings [ ' localArtworkSize ' ] ,
' none-100-0-0.png ' if format == " png " else f ' 000000- { self . settings [ " jpegImageQuality " ] } -0-0.jpg '
)
else :
url = track . album [ ' picUrl ' ]
if format != " jpg " :
continue
else :
url = " https://e-cdns-images.dzcdn.net/images/cover/ {} / {} x {} - {} " . format (
track . album [ ' pic ' ] ,
self . settings [ ' localArtworkSize ' ] , self . settings [ ' localArtworkSize ' ] ,
' none-100-0-0.png ' if format == " png " else f ' 000000- { self . settings [ " jpegImageQuality " ] } -0-0.jpg '
)
2020-08-15 19:34:10 +00:00
result [ ' albumURLs ' ] . append ( { ' url ' : url , ' ext ' : format } )
result [ ' albumPath ' ] = os . path . join ( coverPath ,
f " { settingsRegexAlbum ( self . settings [ ' coverImageTemplate ' ] , track . album , self . settings , trackAPI_gw [ ' _EXTRA_PLAYLIST ' ] if ' _EXTRA_PLAYLIST ' in trackAPI_gw else None ) } " )
# Save artist art
if artistPath :
result [ ' artistURLs ' ] = [ ]
for format in self . settings [ ' localArtworkFormat ' ] . split ( " , " ) :
if format in [ " png " , " jpg " ] :
url = " "
if track . album [ ' mainArtist ' ] [ ' pic ' ] != " " :
url = " https://e-cdns-images.dzcdn.net/images/artist/ {} / {} x {} - {} " . format (
track . album [ ' mainArtist ' ] [ ' pic ' ] , self . settings [ ' localArtworkSize ' ] , self . settings [ ' localArtworkSize ' ] ,
' none-100-0-0.png ' if format == " png " else f ' 000000- { self . settings [ " jpegImageQuality " ] } -0-0.jpg ' )
elif format == " jpg " :
url = " https://e-cdns-images.dzcdn.net/images/artist// {} x {} - {} " . format (
self . settings [ ' localArtworkSize ' ] , self . settings [ ' localArtworkSize ' ] , f ' 000000- { self . settings [ " jpegImageQuality " ] } -0-0.jpg ' )
if url :
result [ ' artistURLs ' ] . append ( { ' url ' : url , ' ext ' : format } )
result [ ' artistPath ' ] = os . path . join ( artistPath ,
f " { settingsRegexArtist ( self . settings [ ' artistImageTemplate ' ] , track . album [ ' mainArtist ' ] , self . settings ) } " )
# Remove subfolders from filename and add it to filepath
if os . path . sep in filename :
tempPath = filename [ : filename . rfind ( os . path . sep ) ]
filepath = os . path . join ( filepath , tempPath )
filename = filename [ filename . rfind ( os . path . sep ) + len ( os . path . sep ) : ]
# Make sure the filepath exsists
makedirs ( filepath , exist_ok = True )
writepath = os . path . join ( filepath , filename + extensions [ track . selectedFormat ] )
# Save lyrics in lrc file
2020-08-16 11:25:25 +00:00
if self . settings [ ' syncedLyrics ' ] and track . lyrics [ ' sync ' ] :
2020-08-16 11:58:14 +00:00
if not os . path . isfile ( os . path . join ( filepath , filename + ' .lrc ' ) ) or self . settings [ ' overwriteFile ' ] in [ ' y ' , ' t ' ] :
2020-08-15 19:34:10 +00:00
with open ( os . path . join ( filepath , filename + ' .lrc ' ) , ' wb ' ) as f :
f . write ( track . lyrics [ ' sync ' ] . encode ( ' utf-8 ' ) )
trackAlreadyDownloaded = os . path . isfile ( writepath )
2020-08-21 12:46:34 +00:00
if not trackAlreadyDownloaded and self . settings [ ' overwriteFile ' ] == ' e ' :
exts = [ ' .mp3 ' , ' .flac ' , ' .opus ' , ' .m4a ' ]
baseFilename = os . path . join ( filepath , filename )
for ext in exts :
trackAlreadyDownloaded = os . path . isfile ( baseFilename + ext )
if trackAlreadyDownloaded :
break
2020-08-15 19:34:10 +00:00
if trackAlreadyDownloaded and self . settings [ ' overwriteFile ' ] == ' b ' :
baseFilename = os . path . join ( filepath , filename )
i = 1
currentFilename = baseFilename + ' ( ' + str ( i ) + ' ) ' + extensions [ track . selectedFormat ]
while os . path . isfile ( currentFilename ) :
i + = 1
currentFilename = baseFilename + ' ( ' + str ( i ) + ' ) ' + extensions [ track . selectedFormat ]
trackAlreadyDownloaded = False
writepath = currentFilename
if extrasPath :
if not self . extrasPath :
self . extrasPath = extrasPath
result [ ' extrasPath ' ] = extrasPath
# Data for m3u file
2020-08-15 21:36:32 +00:00
result [ ' filename ' ] = writepath [ len ( extrasPath ) : ]
2020-08-15 19:34:10 +00:00
# Save playlist cover
if track . playlist :
2020-08-15 21:36:32 +00:00
if not len ( self . playlistURLs ) :
2020-08-21 12:40:36 +00:00
if track . playlist [ ' pic ' ] :
2020-08-15 21:36:32 +00:00
for format in self . settings [ ' localArtworkFormat ' ] . split ( " , " ) :
if format in [ " png " , " jpg " ] :
2020-08-21 12:40:36 +00:00
url = " {} / {} x {} - {} " . format (
track . playlist [ ' pic ' ] ,
self . settings [ ' localArtworkSize ' ] , self . settings [ ' localArtworkSize ' ] ,
' none-100-0-0.png ' if format == " png " else f ' 000000- { self . settings [ " jpegImageQuality " ] } -0-0.jpg '
)
2020-08-15 21:36:32 +00:00
self . playlistURLs . append ( { ' url ' : url , ' ext ' : format } )
else :
self . playlistURLs . append ( { ' url ' : track . playlist [ ' picUrl ' ] , ' ext ' : ' jpg ' } )
if not self . playlistPath :
track . playlist [ ' id ' ] = " pl_ " + str ( trackAPI_gw [ ' _EXTRA_PLAYLIST ' ] [ ' id ' ] )
track . playlist [ ' genre ' ] = [ " Compilation " , ]
track . playlist [ ' bitrate ' ] = selectedFormat
track . playlist [ ' dateString ' ] = formatDate ( track . playlist [ ' date ' ] , self . settings [ ' dateFormat ' ] )
self . playlistPath = f " { settingsRegexAlbum ( self . settings [ ' coverImageTemplate ' ] , track . playlist , self . settings , trackAPI_gw [ ' _EXTRA_PLAYLIST ' ] ) } "
2020-08-15 19:34:10 +00:00
if not trackAlreadyDownloaded or self . settings [ ' overwriteFile ' ] == ' y ' :
logger . info ( f " [ { track . mainArtist [ ' name ' ] } - { track . title } ] Downloading the track " )
2020-08-15 21:03:05 +00:00
track . downloadUrl = self . dz . get_track_stream_url ( track . id , track . MD5 , track . mediaVersion , track . selectedFormat )
2020-08-15 19:34:10 +00:00
def downloadMusic ( track , trackAPI_gw ) :
try :
with open ( writepath , ' wb ' ) as stream :
2020-08-15 21:12:26 +00:00
self . streamTrack ( stream , track )
2020-08-15 19:34:10 +00:00
except DownloadCancelled :
remove ( writepath )
raise DownloadCancelled
2020-09-07 10:09:46 +00:00
except ( request_exception . HTTPError , DownloadEmpty ) :
2020-08-15 19:34:10 +00:00
remove ( writepath )
if track . fallbackId != " 0 " :
logger . warn ( f " [ { track . mainArtist [ ' name ' ] } - { track . title } ] Track not available, using fallback id " )
newTrack = self . dz . get_track_gw ( track . fallbackId )
track . parseEssentialData ( self . dz , newTrack )
return False
elif not track . searched and self . settings [ ' fallbackSearch ' ] :
2020-08-15 21:03:05 +00:00
logger . warn ( f " [ { track . mainArtist [ ' name ' ] } - { track . title } ] Track not available, searching for alternative " )
searchedId = self . dz . get_track_from_metadata ( track . mainArtist [ ' name ' ] , track . title , track . album [ ' title ' ] )
2020-08-18 13:13:32 +00:00
if searchedId != " 0 " :
2020-08-15 19:34:10 +00:00
newTrack = self . dz . get_track_gw ( searchedId )
track . parseEssentialData ( self . dz , newTrack )
track . searched = True
return False
else :
raise DownloadFailed ( " notAvailableNoAlternative " )
else :
raise DownloadFailed ( " notAvailable " )
2020-09-07 10:09:46 +00:00
except ( request_exception . ConnectionError , request_exception . ChunkedEncodingError ) as e :
remove ( writepath )
2020-08-15 19:34:10 +00:00
logger . warn ( f " [ { track . mainArtist [ ' name ' ] } - { track . title } ] Error while downloading the track, trying again in 5s... " )
sleep ( 5 )
return downloadMusic ( track , trackAPI_gw )
except Exception as e :
2020-09-07 10:09:46 +00:00
remove ( writepath )
2020-09-10 09:43:32 +00:00
logger . exception ( f " [ { track . mainArtist [ ' name ' ] } - { track . title } ] Error while downloading the track, you should report this to the developers: { str ( e ) } " )
2020-08-15 19:34:10 +00:00
raise e
return True
try :
trackDownloaded = downloadMusic ( track , trackAPI_gw )
except DownloadFailed as e :
2020-08-15 21:03:05 +00:00
raise e
2020-08-15 19:34:10 +00:00
except Exception as e :
raise e
if not trackDownloaded :
return self . download ( trackAPI_gw , track )
else :
logger . info ( f " [ { track . mainArtist [ ' name ' ] } - { track . title } ] Skipping track as it ' s already downloaded " )
2020-08-15 21:03:05 +00:00
self . completeTrackPercentage ( )
2020-08-15 19:34:10 +00:00
# Adding tags
if ( not trackAlreadyDownloaded or self . settings [ ' overwriteFile ' ] in [ ' t ' , ' y ' ] ) and not track . localTrack :
logger . info ( f " [ { track . mainArtist [ ' name ' ] } - { track . title } ] Applying tags to the track " )
if track . selectedFormat in [ 3 , 1 , 8 ] :
tagID3 ( writepath , track , self . settings [ ' tags ' ] )
elif track . selectedFormat == 9 :
try :
tagFLAC ( writepath , track , self . settings [ ' tags ' ] )
except FLACNoHeaderError :
remove ( writepath )
logger . warn ( f " [ { track . mainArtist [ ' name ' ] } - { track . title } ] Track not available in FLAC, falling back if necessary " )
2020-08-15 21:12:26 +00:00
self . removeTrackPercentage ( )
2020-08-15 21:03:05 +00:00
track . filesizes [ ' FILESIZE_FLAC ' ] = " 0 "
2020-08-22 11:35:24 +00:00
track . filesizes [ ' FILESIZE_FLAC_TESTED ' ] = True
2020-08-15 19:34:10 +00:00
return self . download ( trackAPI_gw , track )
if track . searched :
2020-08-15 21:03:05 +00:00
result [ ' searched ' ] = f " { track . mainArtist [ ' name ' ] } - { track . title } "
2020-08-15 19:34:10 +00:00
2020-08-17 09:04:35 +00:00
logger . info ( f " [ { track . mainArtist [ ' name ' ] } - { track . title } ] Track download completed \n { writepath } " )
2020-08-15 19:34:10 +00:00
self . queueItem . downloaded + = 1
if self . interface :
2020-08-16 08:41:18 +00:00
self . interface . send ( " updateQueue " , { ' uuid ' : self . queueItem . uuid , ' downloaded ' : True , ' downloadPath ' : writepath } )
2020-08-15 19:34:10 +00:00
return result
def getPreferredBitrate ( self , track ) :
if track . localTrack :
return 0
fallback = self . settings [ ' fallbackBitrate ' ]
formats_non_360 = {
9 : " FLAC " ,
3 : " MP3_320 " ,
1 : " MP3_128 " ,
}
formats_360 = {
15 : " MP4_RA3 " ,
14 : " MP4_RA2 " ,
13 : " MP4_RA1 " ,
}
if not fallback :
error_num = - 100
formats = formats_360
formats . update ( formats_non_360 )
elif int ( self . bitrate ) in formats_360 :
error_num = - 200
formats = formats_360
else :
error_num = 8
formats = formats_non_360
for format_num , format in formats . items ( ) :
if format_num < = int ( self . bitrate ) :
2020-08-22 11:35:24 +00:00
if f " FILESIZE_ { format } " in track . filesizes :
if int ( track . filesizes [ f " FILESIZE_ { format } " ] ) != 0 :
return format_num
elif not track . filesizes [ f " FILESIZE_ { format } _TESTED " ] :
request = get ( self . dz . get_track_stream_url ( track . id , track . MD5 , track . mediaVersion , format_num ) , stream = True )
try :
request . raise_for_status ( )
return format_num
2020-09-07 10:09:46 +00:00
except request_exception . HTTPError : # if the format is not available, Deezer returns a 403 error
2020-08-22 11:35:24 +00:00
pass
if fallback :
continue
2020-08-15 19:34:10 +00:00
else :
2020-08-22 11:35:24 +00:00
return error_num
2020-08-15 19:34:10 +00:00
return error_num # fallback is enabled and loop went through all formats
2020-08-15 21:12:26 +00:00
def streamTrack ( self , stream , track ) :
2020-08-15 19:34:10 +00:00
if self . queueItem . cancel : raise DownloadCancelled
try :
2020-08-15 21:03:05 +00:00
request = get ( track . downloadUrl , headers = self . dz . http_headers , stream = True , timeout = 30 )
2020-09-07 10:09:46 +00:00
except request_exception . ConnectionError :
2020-08-15 19:34:10 +00:00
sleep ( 2 )
2020-08-15 21:12:26 +00:00
return self . streamTrack ( stream , track )
2020-08-15 19:34:10 +00:00
request . raise_for_status ( )
2020-08-15 21:03:05 +00:00
blowfish_key = str . encode ( self . dz . _get_blowfish_key ( str ( track . id ) ) )
2020-08-15 19:34:10 +00:00
complete = int ( request . headers [ " Content-Length " ] )
2020-08-15 21:36:32 +00:00
if complete == 0 :
raise DownloadEmpty
2020-08-15 19:34:10 +00:00
chunkLength = 0
percentage = 0
i = 0
for chunk in request . iter_content ( 2048 ) :
if self . queueItem . cancel : raise DownloadCancelled
if i % 3 == 0 and len ( chunk ) == 2048 :
chunk = Blowfish . new ( blowfish_key , Blowfish . MODE_CBC , b " \x00 \x01 \x02 \x03 \x04 \x05 \x06 \x07 " ) . decrypt ( chunk )
stream . write ( chunk )
chunkLength + = len ( chunk )
2020-08-15 21:12:26 +00:00
if isinstance ( self . queueItem , QISingle ) :
2020-08-15 19:34:10 +00:00
percentage = ( chunkLength / complete ) * 100
self . downloadPercentage = percentage
else :
2020-08-15 21:12:26 +00:00
chunkProgres = ( len ( chunk ) / complete ) / self . queueItem . size * 100
2020-08-15 19:34:10 +00:00
self . downloadPercentage + = chunkProgres
self . updatePercentage ( )
i + = 1
def updatePercentage ( self ) :
if round ( self . downloadPercentage ) != self . lastPercentage and round ( self . downloadPercentage ) % 2 == 0 :
self . lastPercentage = round ( self . downloadPercentage )
self . queueItem . progress = self . lastPercentage
if self . interface :
self . interface . send ( " updateQueue " , { ' uuid ' : self . queueItem . uuid , ' progress ' : self . lastPercentage } )
def completeTrackPercentage ( self ) :
if isinstance ( self . queueItem , QISingle ) :
self . downloadPercentage = 100
else :
self . downloadPercentage + = ( 1 / self . queueItem . size ) * 100
self . updatePercentage ( )
def removeTrackPercentage ( self ) :
if isinstance ( self . queueItem , QISingle ) :
self . downloadPercentage = 0
else :
self . downloadPercentage - = ( 1 / self . queueItem . size ) * 100
self . updatePercentage ( )
def downloadWrapper ( self , trackAPI_gw ) :
track = {
2020-08-15 21:03:05 +00:00
' id ' : trackAPI_gw [ ' SNG_ID ' ] ,
' title ' : trackAPI_gw [ ' SNG_TITLE ' ] + ( trackAPI_gw [ ' VERSION ' ] if ' VERSION ' in trackAPI_gw and trackAPI_gw [ ' VERSION ' ] and not trackAPI_gw [ ' VERSION ' ] in trackAPI_gw [ ' SNG_TITLE ' ] else " " ) ,
' artist ' : trackAPI_gw [ ' ART_NAME ' ]
2020-08-15 19:34:10 +00:00
}
try :
result = self . download ( trackAPI_gw )
except DownloadCancelled :
return None
except DownloadFailed as error :
2020-08-15 21:03:05 +00:00
logger . error ( f " [ { track [ ' artist ' ] } - { track [ ' title ' ] } ] { error . message } " )
2020-08-15 19:34:10 +00:00
result = { ' error ' : {
' message ' : error . message ,
' errid ' : error . errid ,
' data ' : track
} }
except Exception as e :
2020-08-15 21:03:05 +00:00
logger . exception ( f " [ { track [ ' artist ' ] } - { track [ ' title ' ] } ] { str ( e ) } " )
2020-08-15 19:34:10 +00:00
result = { ' error ' : {
' message ' : str ( e ) ,
' data ' : track
} }
if ' error ' in result :
self . completeTrackPercentage ( )
self . queueItem . failed + = 1
2020-08-16 10:29:51 +00:00
self . queueItem . errors . append ( result [ ' error ' ] )
2020-08-15 21:03:05 +00:00
if self . interface :
2020-08-15 19:34:10 +00:00
error = result [ ' error ' ]
2020-08-15 21:03:05 +00:00
self . interface . send ( " updateQueue " , {
2020-08-15 19:34:10 +00:00
' uuid ' : self . queueItem . uuid ,
' failed ' : True ,
' data ' : error [ ' data ' ] ,
' error ' : error [ ' message ' ] ,
' errid ' : error [ ' errid ' ] if ' errid ' in error else None
} )
return result
class DownloadError ( Exception ) :
""" Base class for exceptions in this module. """
pass
class DownloadFailed ( DownloadError ) :
def __init__ ( self , errid ) :
self . errid = errid
self . message = errorMessages [ self . errid ]
class DownloadCancelled ( DownloadError ) :
pass
2020-08-15 21:36:32 +00:00
class DownloadEmpty ( DownloadError ) :
pass