Advertisement
hammad9860

spotify script

Jul 30th, 2023
1,560
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 17.65 KB | None | 0 0
  1. import os
  2. import re
  3. import subprocess
  4. import spotipy
  5. import music_tag
  6. import ctypes
  7. import time
  8. from spotipy.oauth2 import SpotifyClientCredentials
  9. from dotenv import load_dotenv
  10. from collections import Counter
  11. from mutagen.mp3 import MP3
  12. from datetime import datetime, timedelta
  13. from colorama import init, Fore, Back, Style
  14. from send2trash import send2trash
  15.  
  16. class Container:
  17.   def __init__(self):
  18.     self.local_library_folder = "D:\Music"
  19.     self.local_playlist_folder = ''
  20.     self.local_temp_download_folder = "D:\Music\[Other]\DownOnSpot"
  21.     self.allowed_extensions = ['.mp3']
  22.    
  23.     self.playlist_url = ''
  24.     self.playlist_id = ''
  25.     self.playlist_name = ''
  26.  
  27.     self.spotify_tracks_raw = []
  28.     self.spotify_tracks_already_downloaded = []
  29.     self.spotify_tracks_incomplete = []
  30.     self.spotify_tracks_uploaded = []
  31.     self.spotify_tracks_unavailable = []
  32.     self.spotify_tracks_deleted = []
  33.     self.spotify_tracks_to_download = []
  34.    
  35.     self.local_tracks_raw = []
  36.     self.local_tracks_already_downloaded = []
  37.     self.local_tracks_duplicate = []
  38.     self.local_tracks_extra = []
  39.     self.local_tracks_uploaded = []
  40.    
  41.     self.currently_downloading_track = {}
  42.     self.newly_downloaded_track = {}
  43.     self.completed_index = 0
  44.     self.progress_bar_text = ''
  45.  
  46.     self.script_owner = 'HammadXP'
  47.     self.region = 'PK'
  48.  
  49.     self.playlists = {
  50.         'bollywood': 'https://open.spotify.com/playlist/2V6z0Yv2GSxYKhTlevre0J?si=b99b1fec8cf0492d',
  51.         # 'bollywood_classic': 'https://open.spotify.com/playlist/0VUM3gcOCWTONPDLFKOqcT?si=6e8ec6e44d6741c4',
  52.         # 'bollywood_more_previous': 'https://open.spotify.com/playlist/28Ftlt3xLfZAkIxxVpZgLL?si=4ef871e20bcf4936',
  53.         'bollywood_previous': 'https://open.spotify.com/playlist/2vLAwXfPJJLT9zCZiBHLd7?si=a709e13fe5e1433f',
  54.         'indian_hip_hop': 'https://open.spotify.com/playlist/71eOYF6sSARCrtmihyQEUP?si=7b72c2366ceb438b',
  55.         'indian_pop': 'https://open.spotify.com/playlist/1MiOoq94f0YmJi5MYa9SwR?si=35f1bc8f275d44bf',
  56.         # 'just_for_me': 'https://open.spotify.com/playlist/1J1ZhkZMKLttC1WhQegFoK?si=4739c9d0dc9944bf',
  57.         'just_music': 'https://open.spotify.com/playlist/6X4aonsxqtHRFPIc37nln5?si=be7787a1028a4a61',
  58.         'movie_soundtracks': 'https://open.spotify.com/playlist/4NSSc59unC5sBiByLDJb1D?si=44b8e3265e944d4c',
  59.         'movie_tracks': 'https://open.spotify.com/playlist/78fHA86h7O84wBL4cXXKsN?si=d4a7f97eaed747ff',
  60.         'not_bad': 'https://open.spotify.com/playlist/3sq8eo9pnCvPcxjYIQo0VN?si=d61c4dae712141ef',
  61.         # 'ost': 'https://open.spotify.com/playlist/57rWznXfKMWOCxPRrPtuHN?si=7ef95cbfff71410d',
  62.         'pakistani_classic': 'https://open.spotify.com/playlist/2O73qJgmZfB57rKnpo7Jzn?si=77800e4812a74c5e',
  63.         'pakistani_pop': 'https://open.spotify.com/playlist/0XlE4fNzKuesDnSVmBb9bh?si=b18b659227204df9',
  64.         'pop_music': 'https://open.spotify.com/playlist/2BOn4rcEwKZ4HxaHGOMpIf?si=d681376f9e6b42f5',
  65.         'punjabi': 'https://open.spotify.com/playlist/0GWg1KQSiIAjquU9LANpYD?si=4bb8c09f9a0244ba',
  66.         'shorts_less': 'https://open.spotify.com/playlist/0jLk6UGikPGpbbApFg7NvU?si=1bd4aece3d6240aa',
  67.         'shorts_music': 'https://open.spotify.com/playlist/5rCPO6bFNZfwnZSC857LeS?si=823bb76b528a4e26',
  68.         'solo_music': 'https://open.spotify.com/playlist/2NYdWNXYaOhuOLLWO2aY1X?si=673fab0500504229',
  69.         'soundtracks': 'https://open.spotify.com/playlist/5wn5aufTRaLoYRykTl57u4?si=861d3c6683a943a2',
  70.         'soundtracks_codm': 'https://open.spotify.com/playlist/0yVjrLYrbraAWGfr7xWAZt?si=4030e456b6e748ad',
  71.         # 'superhero_music': 'https://open.spotify.com/playlist/2JilFHyef9EhsER3R6k6AR?si=93efd1985e3a4312',
  72.         'uplifting_music': 'https://open.spotify.com/playlist/78u4RkP3zK9malm09IM9Ku?si=fbf691d3c4974d37',
  73.     }
  74.  
  75.     self.playlists_test = {
  76.       'DownOnSpot (Test)': 'https://open.spotify.com/playlist/4K1lpHuBT9j3TUbeHw51zt?si=570766aa646f440b',
  77.     }
  78.  
  79.     self.playlists_2 = {
  80.       'indian_pop': 'https://open.spotify.com/playlist/1MiOoq94f0YmJi5MYa9SwR?si=35f1bc8f275d44bf'
  81.       # 'pakistani_pop': 'https://open.spotify.com/playlist/0XlE4fNzKuesDnSVmBb9bh?si=b18b659227204df9',
  82.       # 'shorts_less': 'https://open.spotify.com/playlist/0jLk6UGikPGpbbApFg7NvU?si=1bd4aece3d6240aa',
  83.     }
  84.  
  85.   ######################################################
  86.  
  87.   def create_session(self):
  88.     load_dotenv()
  89.  
  90.     client_id = os.getenv("CLIENT_ID", "")
  91.     client_secret = os.getenv("CLIENT_SECRET", "")
  92.  
  93.     client_credentials_manager = SpotifyClientCredentials(client_id=client_id, client_secret=client_secret)
  94.     self.session = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
  95.  
  96.   def update_window_title(self, text):
  97.     ctypes.windll.kernel32.SetConsoleTitleW(f"Spotify Script by {self.script_owner} | {text}")
  98.  
  99.   ######################################################
  100.  
  101.   def get_playlist_id(self):
  102.     if match := re.match(r"https://open.spotify.com/playlist/(.*)\?", self.playlist_url):
  103.       self.playlist_id = match.groups()[0]
  104.     else:
  105.       raise ValueError("Bad URL format !!!")
  106.  
  107.   def get_playlist_name(self):
  108.     self.playlist_name = self.session.user_playlist(user=None, playlist_id=self.playlist_id, fields="name")['name']
  109.  
  110.   def get_spotify_tracks_raw(self):
  111.     self.spotify_tracks_raw = []
  112.    
  113.     response = self.session.playlist_tracks(self.playlist_id, market=self.region)
  114.     tracks_fetched = response['items']
  115.  
  116.     while response['next']:
  117.       response = self.session.next(response)
  118.       tracks_fetched.extend(response['items'])
  119.  
  120.     for track in tracks_fetched:
  121.       title = track['track']['name']
  122.       artist = ", ".join([artist['name'] for artist in track['track']['artists']])
  123.       album = track['track']['album']['name']
  124.       duration = track['track']['duration_ms']
  125.       release_date = track['track']['album']['release_date']
  126.       added_at = track['added_at']      
  127.       track_url = track['track']['external_urls'].get('spotify', False)
  128.       track_id = track['track']['id'] # For local -> 'None'
  129.       is_local = track['track']['is_local']
  130.       is_playable = track['track'].get('is_playable', True)
  131.  
  132.       self.spotify_tracks_raw.append({'title': title, 'artist': artist, 'album': album, 'duration': duration, 'release_date': release_date, 'added_at': added_at, 'track_url': track_url, 'track_id': track_id, 'is_local': is_local, 'is_playable': is_playable})
  133.  
  134.     self.spotify_tracks_raw.sort(key=lambda a: (a['title'].lower(), a['artist'].lower()))
  135.  
  136.   def get_local_tracks_raw(self):
  137.     self.local_tracks_raw = []
  138.    
  139.     for folder, subfolders, files in os.walk(self.local_playlist_folder):
  140.       for file in files:
  141.         file_path = os.path.join(folder, file)
  142.         file_name = os.path.splitext(file)[0]
  143.         file_extension = os.path.splitext(file)[1]
  144.  
  145.         if file_extension in self.allowed_extensions:
  146.           music_file = music_tag.load_file(file_path)
  147.           music_file_mutagen = MP3(file_path)
  148.  
  149.           title = str(music_file['title'])
  150.           artist = str(music_file['artist'])
  151.           album = str(music_file['album'])
  152.           duration = music_file_mutagen.info.length * 1000
  153.           comment_raw = str(music_file['comment'])
  154.           comment = comment_raw.split(',')[0]
  155.  
  156.           self.local_tracks_raw.append({"title": title, "artist": artist, "album": album, "duration": duration, "comment": comment, "file_name": file_name, "file_path": file_path})
  157.  
  158.     self.local_tracks_raw.sort(key=lambda a: (a['title'].lower(), a['artist'].lower()))
  159.  
  160.   def create_lists(self):
  161.     for spotify_track in self.spotify_tracks_raw:
  162.       for local_track in self.local_tracks_raw:
  163.         if spotify_track['title'] == local_track['title'] and spotify_track['artist'] == local_track['artist'] and spotify_track['album'] == local_track['album']:
  164.            
  165.           # get list of tracks that have already been downloaded
  166.           if spotify_track['track_id'] == local_track['comment']:
  167.             self.spotify_tracks_already_downloaded.append(spotify_track) if spotify_track not in self.spotify_tracks_already_downloaded else None
  168.             self.local_tracks_already_downloaded.append(local_track) if local_track not in self.local_tracks_already_downloaded else None
  169.  
  170.           # get list of incomplete tracks for re-download
  171.           if spotify_track['duration'] != local_track['duration']:
  172.             if (spotify_track['duration'] - local_track['duration']) > 1000: # duration difference in milliseconds, so 1000 = 1s
  173.               self.spotify_tracks_incomplete.append(spotify_track) if spotify_track not in self.spotify_tracks_incomplete else None
  174.  
  175.           # get list of uploaded tracks
  176.           if spotify_track['is_local']:
  177.             self.spotify_tracks_uploaded.append(spotify_track) if spotify_track not in self.spotify_tracks_uploaded else None
  178.             self.local_tracks_uploaded.append(local_track) if local_track not in self.local_tracks_uploaded else None
  179.  
  180.       # get list of tracks that have been deleted from spotify
  181.       if spotify_track['title'] == '' and spotify_track['artist'] == '' and spotify_track['album'] == '':
  182.         self.spotify_tracks_deleted.append(spotify_track) if spotify_track not in self.spotify_tracks_deleted else None
  183.      
  184.       # get list of unavailable tracks
  185.       if not spotify_track['is_playable']:
  186.         self.spotify_tracks_unavailable.append(spotify_track) if spotify_track not in self.spotify_tracks_unavailable else None
  187.  
  188.     # get list of extra local tracks that should be removed locally
  189.     self.local_tracks_extra = [item for item in self.local_tracks_raw if item not in self.local_tracks_already_downloaded]
  190.  
  191.     # prevent uploaded tracks from getting added to extra local tracks list
  192.     self.local_tracks_extra = [item for item in self.local_tracks_extra if item not in self.local_tracks_uploaded]
  193.        
  194.     # get list of tracks that have been downloaded twice with same ids but different filenames
  195.     # checked_ids = set()
  196.  
  197.     # for local_track in self.local_tracks_raw:
  198.     #   if local_track['comment'] != '' and local_track['comment'] in checked_ids:
  199.     #     self.local_tracks_duplicate.append(local_track)
  200.     #     self.local_tracks_duplicate.append(next(item for item in self.local_tracks_duplicate if item['comment'] == local_track['comment']))
  201.  
  202.     #   elif local_track['comment'] != '':
  203.     #     checked_ids.add(local_track['comment'])
  204.    
  205.     # get list of tracks to download
  206.     self.spotify_tracks_to_download = self.spotify_tracks_raw + self.spotify_tracks_incomplete
  207.  
  208.     for item in self.spotify_tracks_already_downloaded + self.spotify_tracks_uploaded + self.spotify_tracks_deleted + self.spotify_tracks_unavailable:
  209.       if item in self.spotify_tracks_to_download:
  210.         self.spotify_tracks_to_download.remove(item)
  211.  
  212.   def duplicate_proof_filename(self):
  213.     for spotify_track in self.spotify_tracks_to_download:
  214.       spotify_track['file_name_final'] = spotify_track['title']
  215.  
  216.       for local_track in self.local_tracks_raw:
  217.         if spotify_track['title'].lower() == local_track['title'].lower():
  218.           spotify_track['file_name_final'] = f"{spotify_track['title']} (by {spotify_track['artist']})"
  219.        
  220.         if spotify_track['title'].lower() == local_track['title'].lower() and spotify_track['artist'].lower() == local_track['artist'].lower():
  221.           spotify_track['file_name_final'] = f"{spotify_track['title']} (by {spotify_track['artist']}) ({spotify_track['album']})" # for infinity war
  222.  
  223.   def remove_local_tracks_duplicate(self):
  224.     for local_track in self.local_tracks_duplicate:
  225.       file_path = local_track['file_path']
  226.       does_exist = os.path.exists(file_path)
  227.  
  228.       if does_exist:
  229.         send2trash(file_path)
  230.  
  231.   def remove_local_tracks_extra(self):
  232.     for local_track in self.local_tracks_extra:
  233.       file_path = local_track['file_path']
  234.       does_exist = os.path.exists(file_path)
  235.  
  236.       if does_exist:
  237.         send2trash(file_path)
  238.  
  239.   def empty_local_temporary_download_folder(self):
  240.     for folder, subfolders, files in os.walk(self.local_temp_download_folder):
  241.       for file in files:
  242.         file_path = os.path.join(folder, file)
  243.         send2trash(file_path)
  244.  
  245.   ######################################################
  246.  
  247.   def downloader(self):
  248.     self.update_window_title(self.progress_bar_text)
  249.     os.system(f"down_on_spot \"{self.currently_downloading_track['track_url']}\"")
  250.  
  251.   def get_newly_downloaded_track(self):
  252.     for folder, subfolders, files in os.walk(self.local_temp_download_folder):
  253.       for file in files:
  254.         file_path = os.path.join(folder, file)
  255.         file_name = os.path.splitext(file)[0]
  256.         file_extension = os.path.splitext(file)[1]
  257.         file_name_final = self.currently_downloading_track['file_name_final']
  258.  
  259.         if file_extension in self.allowed_extensions:
  260.           music_file = music_tag.load_file(file_path)
  261.  
  262.           title = str(music_file['title'])
  263.           artist = str(music_file['artist'])
  264.           album = str(music_file['album'])
  265.           comment_raw = str(music_file['comment'])
  266.           comment = comment_raw.split(',')[0]
  267.  
  268.           self.newly_downloaded_track = {"title": title, "artist": artist, "album": album, "comment": comment, "file_name": file_name, "file_path": file_path, "file_extension": file_extension, "file_name_final": file_name_final}
  269.  
  270.   def edit_metadata(self):
  271.     music_file = music_tag.load_file(self.newly_downloaded_track['file_path'])
  272.  
  273.     music_file['title'] = self.currently_downloading_track['title'] # forgot why i needed to edit them
  274.     music_file['artist'] = self.currently_downloading_track['artist']
  275.     music_file['album'] = self.currently_downloading_track['album']
  276.    
  277.     music_file['comment'] = self.currently_downloading_track['track_id']
  278.     music_file.save()
  279.  
  280.   def edit_modification_date(self):
  281.     timestamp_raw = self.currently_downloading_track['added_at']
  282.     timestamp = datetime.strptime(timestamp_raw, "%Y-%m-%dT%H:%M:%SZ")
  283.     timestamp_adjusted = timestamp + timedelta(hours=5)
  284.     timestamp_epoch = timestamp_adjusted.timestamp()
  285.     mdate_P1 = timestamp_adjusted.strftime("%Y-%m-%d")
  286.     mdate_P2 = timestamp_adjusted.strftime("%Y-%m-%d %H:%M:%S")
  287.  
  288.     os.utime(self.newly_downloaded_track['file_path'], (timestamp_epoch, timestamp_epoch))
  289.  
  290.   def rename_move_downloaded_track(self):
  291.     file_name_new = re.sub(r'[\\/*?:"<>|]', "", self.newly_downloaded_track['file_name_final'])
  292.  
  293.     file_path_old = self.newly_downloaded_track['file_path']
  294.     file_path_new = os.path.join(self.local_playlist_folder, file_name_new + self.newly_downloaded_track['file_extension'])
  295.  
  296.     if not os.path.exists(self.local_playlist_folder):
  297.       os.makedirs(self.local_playlist_folder)
  298.  
  299.     os.replace(file_path_old, file_path_new)
  300.  
  301.   def download_handler(self):
  302.     for spotify_track in self.spotify_tracks_to_download:
  303.       self.currently_downloading_track = spotify_track
  304.  
  305.       self.progress_bar_text = f"\nPlaylist: {self.playlist_name} (Total Songs: {len(self.spotify_tracks_raw)} | To Download: {len(self.spotify_tracks_to_download) - self.completed_index}) (Unavailable: {len(self.spotify_tracks_unavailable)}) | Downloading: {spotify_track['title']}\n"
  306.  
  307.       self.downloader()
  308.       self.get_newly_downloaded_track()
  309.       self.edit_metadata()
  310.       self.edit_modification_date()
  311.       self.rename_move_downloaded_track()
  312.  
  313.       self.completed_index += 1
  314.  
  315.     if not self.spotify_tracks_to_download:
  316.       print(f"{self.playlist_name.ljust(30)} {Fore.GREEN + 'DOWNLOADED' + Style.RESET_ALL}")
  317.  
  318.     if self.spotify_tracks_to_download == self.completed_index and len(self.spotify_tracks_to_download) > 0:
  319.       print(f"{self.playlist_name.ljust(30)} {Fore.GREEN + 'DOWNLOADED' + Fore.YELLOW + ' (Unavailable skipped)' + Style.RESET_ALL}")
  320.  
  321.   def reset_lists(self):
  322.     self.spotify_tracks_raw = []
  323.     self.spotify_tracks_already_downloaded = []
  324.     self.spotify_tracks_incomplete = []
  325.     self.spotify_tracks_uploaded = []
  326.     self.spotify_tracks_unavailable = []
  327.     self.spotify_tracks_deleted = []
  328.     self.spotify_tracks_to_download = []
  329.    
  330.     self.local_tracks_raw = []
  331.     self.local_tracks_already_downloaded = []
  332.     self.local_tracks_duplicate = []
  333.     self.local_tracks_extra = []
  334.     self.local_tracks_uploaded = []
  335.    
  336.     self.completed_index = 0
  337.  
  338.   ######################################################
  339.  
  340.   def app_handler(self):
  341.     self.__init__()
  342.     self.create_session()
  343.     init() # For colorama
  344.  
  345.     for _, playlist_url in self.playlists.items():
  346.       self.playlist_url = playlist_url;
  347.       self.get_playlist_id();
  348.       self.get_playlist_name();
  349.       self.local_playlist_folder = os.path.join(self.local_library_folder, self.playlist_name)
  350.      
  351.       self.update_window_title(f"Processing: {self.playlist_name}")
  352.       self.get_spotify_tracks_raw()
  353.       self.get_local_tracks_raw()
  354.       self.create_lists()
  355.       # return
  356.       self.duplicate_proof_filename()
  357.  
  358.       # self.remove_local_tracks_duplicate()
  359.       self.remove_local_tracks_extra()
  360.       self.empty_local_temporary_download_folder()
  361.      
  362.       self.download_handler()
  363.       self.reset_lists()
  364.  
  365.     self.update_window_title('Finished.')
  366.  
  367. # START
  368.  
  369. app = Container()
  370. app.app_handler()
  371.  
  372. input('\nFinished.')
  373.  
  374. ########################################################
  375.  
  376. # Notes:
  377. # is_playable is 'true' on normal tracks, 'false' on unavailable tracks, and is not present on local tracks
  378. # index was added to keep the compare process in sync, but not needed anymore
  379. # sync might also be the reason for lambda sorting
  380. # music_tag showed comment tag as twice
  381. # append method in 'get_spotify_tracks_to_download' was causing infinte loop
  382.  
  383. ########################################################
  384.  
Tags: python
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement