diff --git a/.gitignore b/.gitignore index ed18caf..224e04d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,176 @@ -oauth.json -playlists.json -__pycache__ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ dist/ -spotify2ytmusic/playlists_backup.json -spotify2ytmusic/settings.json -spotify2ytmusic/test.py -.idea/ -_backup.json +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# PyPI configuration file +.pypirc +# sensitive info +oauth.json +raw_headers.txt +playlists.json \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..cc966e1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cSpell.words": ["ytmusicapi"] +} diff --git a/README.md b/README.md index 6b9abdd..69a8961 100644 --- a/README.md +++ b/README.md @@ -15,20 +15,19 @@ project. ## Install Python (you may already have it) -You will need a somewhat recent version of Python 3.10 and above are known to work, +You will need a somewhat recent version of Python 3.10 and above are known to work, 3.8-3.10 might work. ### For Windows -Download Python for Windows from: https://www.python.org/downloads/windows/ - +Download Python for Windows from: You can also use choco to install it: `choco install python` ### For MacOS Run: -``` +```shell brew install python brew install python-tk ``` @@ -37,119 +36,105 @@ Install certificates by doing: Macintosh HD > Applications > Python Folder > double click on "Install Certificates.command" file. -### For Linux - -You probably already have it installed. See your package manager of choice to -install it. - -## Install spotify2ytmusic (via pip) - -This package is available on pip, so you can install it using: - -`pip install spotify2ytmusic` +--- -or: +## Setup Instructions -`python3 -m pip install spotify2ytmusic` +### 1. Clone & Create a Virtual Environment & Install Required Packages -## (Or) Running From Source - -(Not recommended) - -Another option, instead of pip, is to just clone this repo and run directly from the -source. However, you will need the "ytmusicapi" package installed, so you'll probably -want to use pip to install that at the very least. - -To run directly from source: +Start by creating and activating a Python virtual environment to isolate dependencies. ```shell -git clone git@github.com:linsomniac/spotify_to_ytmusic.git +git clone https://github.com/AmidelEst/spotify_to_ytmusic.git cd spotify_to_ytmusic -pip install ytmusicapi -pip install tk # If using the GUI ``` -Then you can prefix the command you want to run with `python3 -m spotify2ytmusic`, for -example: - ```shell -python3 -m spotify2ytmusic gui -python3 -m spotify2ytmusic list_playlists -python3 -m spotify2ytmusic load_liked -[etc...] +python -m venv .venv +.venv\Scripts\activate +pip install ytmusicapi tk ``` -## Graphical UI +--- -If you have installed via PIP, you should be able to run: `s2yt_gui` +### 2. Generate YouTube Music Credentials -Otherwise, if running from source: +To use the YouTube Music API, you need to generate valid credentials. Follow these steps: +![GIF demonstrating how to inquire about credentials in YouTube Music](assets/youtube-music-instructions.gif) -On Windows: `python -m spotify2ytmusic gui` +1. **Log in to YouTube Music**: + Open [YouTube Music](https://music.youtube.com) in Firefox and ensure you are logged in. -Or on Linux: `python3 -m spotify2ytmusic gui` +2. **Open the Inspection Tool**: + Press `F12` to open the browser’s inspection tool. -### Login to YTMusic - Tab 0 +3. **Access the Network Tab**: + Navigate to the **Network** tab and filter by `/browse`. -#### Click the `login` button on the first tab +4. **Select a Request**: + Click on one of the requests under the filtered results and locate the **Request Headers** section. -OR +5. **Toggle RAW View**: + Click on the **RAW** toggle button to view the headers in raw format. -Run `ytmusicapi oauth` in a console. +6. **Copy Headers**: + Right-click, choose **Select All**, and then copy the content. -OR +7. **Paste into `raw_headers.txt`**: + Open the `raw_headers.txt` file located in the main directory of this project and paste the copied content into it. -Run `s2yt_ytoauth` +8. **Run the Script**: -OR + Execute the following command to generate the credentials file: -Run `python -m spotify2ytmusic ytoauth` + ```bash + python spotify2ytmusic/ytmusic_credentials.py + ``` -This will give you a URL, visit that URL and authorize the application. When you are -done with the import you can remove the authorization for this app. +9. **Done**: -This will write a file "oauth.json". Keep this file secret while the app is authorized. -This file includes a logged in session token. + Your YouTube Music credentials are now ready. -ytmusicapi is a dependency of this software and should be installed as part of the "pip -install". +--- -### Backup Your Spotify Playlists - Tab 1 +### 3. Use the GUI for Migration -#### Click the `Backup` button, and wait until it finished and switched to the next tab. +Now you can use the graphical user interface (GUI) Tab 2 -> Tab 6 +to migrate your playlists and liked songs to YouTube Music. -**OR** do all the steps below +Start the GUI with the following command: -Download -[spotify-backup](https://raw.githubusercontent.com/caseychu/spotify-backup/master/spotify-backup.py). +On Windows: python -m spotify2ytmusic gui -Run `spotify-backup.py` and it will help you authorize access to your spotify account. +Or on Linux: python3 -m spotify2ytmusic gui -Run: `python3 spotify-backup.py playlists.json --dump=liked,playlists --format=json` +--- -This will save your playlists and liked songs into the file "playlists.json". +## GUI Features -### Reverse your playlists - Tab 2 -As mentionned below, the original program adds the songs in the 'wrong' order. That's a -feature I don't like, so I created a script to reverse them. It seems to be reliable, -but if you find anything weird, please open an issue. It creates a backup of the -original file just in case anyway. +Once the GUI is running, you can: +- **Backup Your Spotify Playlists**: will save your playlists and liked songs into the file "playlists.json". +- **Load Liked Songs**: Migrate your Spotify liked songs to YouTube Music. +- **List Playlists**: View your playlists and their details. +- **Copy All Playlists**: Migrate all Spotify playlists to YouTube Music. +- **Copy a Specific Playlist**: Select and migrate a specific Spotify playlist to YouTube Music. -Example: `python3 .\reverse_playlist.py ./playlists.json -r` +--- ### Import Your Liked Songs - Tab 3 -#### Click the `import` button, and wait until it finished and switched to the next tab. -It will go through your Spotify liked songs, and like them on YTMusic. It will display -the song from spotify and then the song that it found on YTMusic that it is liking. I've +#### Click the `import` button, and wait until it finished and switched to the next tab + +It will go through your Spotify liked songs, and like them on YTMusic. It will display +the song from Spotify and then the song that it found on YTMusic that it is liking. I've spot-checked my songs and it seems to be doing a good job of matching YTMusic songs with -Spotify. So far I haven't seen a single failure across a couple hundread songs, but more +Spotify. So far I haven't seen a single failure across a couple hundred songs, but more esoteric titles it may have issues with. ### List Your Playlists - Tab 4 -#### Click the `list` button, and wait until it finished and switched to the next tab. +#### Click the `list` button, and wait until it finished and switched to the next tab This will list the playlists you have on both Spotify and YTMusic, so you can individually copy them. @@ -158,14 +143,15 @@ This will list the playlists you have on both Spotify and YTMusic, so you can in You can either copy **all** playlists, or do a more surgical copy of individual playlists. Copying all playlists will use the name of the Spotify playlist as the destination playlist name on YTMusic. -#### To copy all the playlists click the `copy` button, and wait until it finished and switched to the next tab. +#### To copy all the playlists click the `copy` button, and wait until it finished and switched to the next tab **NOTE**: This does not copy the Liked playlist (see above to do that). ### Copy specific Playlist - Tab 6 In the list output, find the "playlist id" (the first column) of the Spotify playlist and of the YTMusic playlist. -#### Then fill both input fields and click the `copy` button. + +#### Then fill both input fields and click the `copy` button The copy playlist will take the name of the YTMusic playlist and will create the playlist if it does not exist, if you start the YTMusic playlist with a "+": @@ -173,20 +159,9 @@ playlist if it does not exist, if you start the YTMusic playlist with a "+": Re-running "copy_playlist" or "load_liked" in the event that it fails should be safe, it will not duplicate entries on the playlist. -## Command Line Usage - -### Login to YTMusic - -`ytmusicapi oauth` or `s2yt_ytoauth` or `python -m spotify2ytmusic ytoauth` - -This will give you a URL, visit that URL and authorize the application. When you are -done with the import you can remove the authorization for this app. +--- -This will write a file "oauth.json". Keep this file secret while the app is authorized. -This file includes a logged in session token. - -ytmusicapi is a dependency of this software and should be installed as part of the "pip -install". +## Command Line Usage ### Backup Your Spotify Playlists @@ -203,31 +178,31 @@ This will save your playlists and liked songs into the file "playlists.json". Run: `s2yt_load_liked` -It will go through your Spotify liked songs, and like them on YTMusic. It will display -the song from spotify and then the song that it found on YTMusic that it is liking. I've +It will go through your Spotify liked songs, and like them on YTMusic. It will display +the song from Spotify and then the song that it found on YTMusic that it is liking. I've spot-checked my songs and it seems to be doing a good job of matching YTMusic songs with -Spotify. So far I haven't seen a single failure across a couple thousand songs, but more +Spotify. So far I haven't seen a single failure across a couple thousand songs, but more esoteric titles it may have issues with. ### Import Your Liked Albums Run: `s2yt_load_liked_albums` -Spotify stores liked albums outside of the "Liked Songs" playlist. This is the command to +Spotify stores liked albums outside of the "Liked Songs" playlist. This is the command to load your liked albums into YTMusic liked songs. ### List Your Playlists Run `s2yt_list_playlists` -This will list the playlists you have on both Spotify and YTMusic. You will need to +This will list the playlists you have on both Spotify and YTMusic. You will need to individually copy them. ### Copy Your Playlists You can either copy **all** playlists, or do a more surgical copy of individual playlists. Copying all playlists will use the name of the Spotify playlist as the destination -playlist name on YTMusic. To copy all playlists, run: +playlist name on YTMusic. To copy all playlists, run: `s2yt_copy_all_playlists` @@ -242,7 +217,7 @@ If you need to create a playlist, you can run: `s2yt_create_playlist ""` -*Or* the copy playlist can take the name of the YTMusic playlist and will create the +_Or_ the copy playlist can take the name of the YTMusic playlist and will create the playlist if it does not exist, if you start the YTMusic playlist with a "+": `s2yt_copy_playlist +` @@ -296,10 +271,10 @@ No, this runs on Linux/Windows/MacOS. - I get "No matching distribution found for spotify2ytmusic". This has been reported in [Issue #39](https://github.com/linsomniac/spotify_to_ytmusic/issues/39#issuecomment-1954432174) - and it seems like a mismatch between python versions. Users there, on MacOS, needed + and it seems like a mismatch between python versions. Users there, on MacOS, needed to install a specific version of Python, and then use the matching version of PIP: - ``` + ```shell brew install python@3.10 brew install python-tk@3.10 pip3.10 install spotify2ytmusic @@ -309,7 +284,7 @@ No, this runs on Linux/Windows/MacOS. Given the Spotify track information, it does a lookup for the album by the same artist on YTMusic, then looks at the first 3 hits looking for a track with exactly the same - name. In the event that it can't find that exact track, it then does a search of songs + name. In the event that it can't find that exact track, it then does a search of songs for the track name by the same artist and simply returns the first hit. The idea is that finding the album and artist and then looking for the exact track match @@ -320,7 +295,7 @@ No, this runs on Linux/Windows/MacOS. - My copy is failing with repeated "ERROR: (Retrying) Server returned HTTP 400: Bad Request". - Try running with "--track-sleep=3" argument to do a 3 second sleep between tracks. This + Try running with "--track-sleep=3" argument to do a 3 second sleep between tracks. This will take much longer, but may succeed where faster rates have failed. ## License @@ -328,6 +303,6 @@ No, this runs on Linux/Windows/MacOS. Creative Commons Zero v1.0 Universal spotify-backup.py licensed under MIT License. -See https://github.com/caseychu/spotify-backup for more information. +See for more information. -[//]: # ( vim: set tw=90 ts=4 sw=4 ai: ) +[//]: # ' vim: set tw=90 ts=4 sw=4 ai: ' diff --git a/assets/youtube-music-instructions.gif b/assets/youtube-music-instructions.gif new file mode 100644 index 0000000..8e9c079 Binary files /dev/null and b/assets/youtube-music-instructions.gif differ diff --git a/raw_headers.txt b/raw_headers.txt new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..92b49e4 Binary files /dev/null and b/requirements.txt differ diff --git a/reverse_playlist.py b/spotify2ytmusic/reverse_playlist.py similarity index 100% rename from reverse_playlist.py rename to spotify2ytmusic/reverse_playlist.py diff --git a/spotify2ytmusic/spotify_backup.py b/spotify2ytmusic/spotify_backup.py index 3d6c45c..5d9bcf3 100644 --- a/spotify2ytmusic/spotify_backup.py +++ b/spotify2ytmusic/spotify_backup.py @@ -1,72 +1,56 @@ #!/usr/bin/env python3 -## A modified version of spotify-backup by 'caseychu' from https://github.com/caseychu/spotify-backup - -import codecs, http.client, http.server, json, re, sys, time, urllib.error, urllib.parse, urllib.request, webbrowser +import codecs +import http.client +import http.server +import json +import re +import sys +import time +import urllib.error +import urllib.parse +import urllib.request +import webbrowser class SpotifyAPI: - # Requires an OAuth token. + """Class to interact with the Spotify API using an OAuth token.""" + + BASE_URL = "https://api.spotify.com/v1/" + def __init__(self, auth): self._auth = auth - # Gets a resource from the Spotify API and returns the object. def get(self, url, params={}, tries=3): - # Construct the correct URL. - if not url.startswith("https://api.spotify.com/v1/"): - url = "https://api.spotify.com/v1/" + url - if params: - url += ("&" if "?" in url else "?") + urllib.parse.urlencode(params) - - # Try the sending off the request a specified number of times before giving up. + """Fetch a resource from Spotify API.""" + url = self._construct_url(url, params) for _ in range(tries): try: - req = urllib.request.Request(url) - req.add_header("Authorization", "Bearer " + self._auth) - res = urllib.request.urlopen(req) - reader = codecs.getreader("utf-8") - return json.load(reader(res)) - except UnicodeDecodeError: - raise + req = self._create_request(url) + return self._read_response(req) except Exception as err: - print("Couldn't load URL: {} ({})".format(url, err)) + print(f"Error fetching URL {url}: {err}") time.sleep(2) - print("Trying again...") - sys.exit(1) + sys.exit("Failed to fetch data from Spotify API after retries.") - # The Spotify API breaks long lists into multiple pages. This method automatically - # fetches all pages and joins them, returning in a single list of objects. def list(self, url, params={}): - last_log_time = time.time() + """Fetch paginated resources and return as a combined list.""" response = self.get(url, params) items = response["items"] while response["next"]: - if time.time() > last_log_time + 15: - last_log_time = time.time() - print(f"Loaded {len(items)}/{response['total']} items") - response = self.get(response["next"]) items += response["items"] return items - # Pops open a browser window for a user to log in and authorize API access. @staticmethod def authorize(client_id, scope): - url = "https://accounts.spotify.com/authorize?" + urllib.parse.urlencode( - { - "response_type": "token", - "client_id": client_id, - "scope": scope, - "redirect_uri": "http://127.0.0.1:{}/redirect".format( - SpotifyAPI._SERVER_PORT - ), - } - ) - print(f"Logging in (click if it doesn't open automatically): {url}") + """Open a browser for user authorization and return SpotifyAPI instance.""" + redirect_uri = f"http://127.0.0.1:{SpotifyAPI._SERVER_PORT}/redirect" + url = SpotifyAPI._construct_auth_url(client_id, scope, redirect_uri) + print(f"Open this link if the browser doesn't open automatically: {url}") webbrowser.open(url) - # Start a simple, local HTTP server to listen for the authorization token... (i.e. a hack). server = SpotifyAPI._AuthorizationServer("127.0.0.1", SpotifyAPI._SERVER_PORT) try: while True: @@ -74,49 +58,74 @@ def authorize(client_id, scope): except SpotifyAPI._Authorization as auth: return SpotifyAPI(auth.access_token) - # The port that the local server listens on. Don't change this, - # as Spotify only will redirect to certain predefined URLs. + @staticmethod + def _construct_auth_url(client_id, scope, redirect_uri): + return ( + "https://accounts.spotify.com/authorize?" + + urllib.parse.urlencode( + { + "response_type": "token", + "client_id": client_id, + "scope": scope, + "redirect_uri": redirect_uri, + } + ) + ) + + def _construct_url(self, url, params): + """Construct a full API URL.""" + if not url.startswith(self.BASE_URL): + url = self.BASE_URL + url + if params: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(params) + return url + + def _create_request(self, url): + """Create an authenticated request.""" + req = urllib.request.Request(url) + req.add_header("Authorization", f"Bearer {self._auth}") + return req + + def _read_response(self, req): + """Read and parse the response.""" + with urllib.request.urlopen(req) as res: + reader = codecs.getreader("utf-8") + return json.load(reader(res)) + _SERVER_PORT = 43019 class _AuthorizationServer(http.server.HTTPServer): def __init__(self, host, port): - http.server.HTTPServer.__init__( - self, (host, port), SpotifyAPI._AuthorizationHandler - ) + super().__init__((host, port), SpotifyAPI._AuthorizationHandler) - # Disable the default error handling. def handle_error(self, request, client_address): raise class _AuthorizationHandler(http.server.BaseHTTPRequestHandler): def do_GET(self): - # The Spotify API has redirected here, but access_token is hidden in the URL fragment. - # Read it using JavaScript and send it to /token as an actual query string... if self.path.startswith("/redirect"): - self.send_response(200) - self.send_header("Content-Type", "text/html") - self.end_headers() - self.wfile.write( - b'' - ) - - # Read access_token and use an exception to kill the server listening... + self._redirect_to_token() elif self.path.startswith("/token?"): - self.send_response(200) - self.send_header("Content-Type", "text/html") - self.end_headers() - self.wfile.write( - b"Thanks! You may now close this window." - ) - - access_token = re.search("access_token=([^&]*)", self.path).group(1) - print(f"Received access token from Spotify: {access_token}") - raise SpotifyAPI._Authorization(access_token) - + self._handle_token() else: self.send_error(404) - # Disable the default logging. + def _redirect_to_token(self): + self.send_response(200) + self.send_header("Content-Type", "text/html") + self.end_headers() + self.wfile.write( + b'' + ) + + def _handle_token(self): + self.send_response(200) + self.send_header("Content-Type", "text/html") + self.end_headers() + self.wfile.write(b"Thanks! You may now close this window.") + access_token = re.search("access_token=([^&]*)", self.path).group(1) + raise SpotifyAPI._Authorization(access_token) + def log_message(self, format, *args): pass @@ -125,96 +134,63 @@ def __init__(self, access_token): self.access_token = access_token -def main(dump="playlists,liked", format="json", file="playlists.json", token=""): - print("Starting backup...") - # If they didn't give a filename, then just prompt them. (They probably just double-clicked.) - while file == "": - file = input("Enter a file name (e.g. playlists.txt): ") - format = file.split(".")[-1] - - # Log into the Spotify API. - if token != "": - spotify = SpotifyAPI(token) - else: - spotify = SpotifyAPI.authorize( - client_id="5c098bcc800e45d49e476265bc9b6934", - scope="playlist-read-private playlist-read-collaborative user-library-read", - ) - - # Get the ID of the logged in user. - print("Loading user info...") - me = spotify.get("me") - print("Logged in as {display_name} ({id})".format(**me)) - user_id_escaped = urllib.parse.quote(me["id"]) +def fetch_user_data(spotify, dump): + """Fetch playlists and liked songs based on the dump parameter.""" playlists = [] liked_albums = [] - # List liked albums and songs if "liked" in dump: print("Loading liked albums and songs...") - liked_tracks = spotify.list( - "users/{user_id}/tracks".format(user_id=user_id_escaped), {"limit": 50} - ) + liked_tracks = spotify.list("me/tracks", {"limit": 50}) liked_albums = spotify.list("me/albums", {"limit": 50}) - playlists += [{"name": "Liked Songs", "tracks": liked_tracks}] + playlists.append({"name": "Liked Songs", "tracks": liked_tracks}) - # List all playlists and the tracks in each playlist if "playlists" in dump: print("Loading playlists...") - playlist_data = spotify.list( - "users/{user_id}/playlists".format(user_id=user_id_escaped), {"limit": 50} - ) - print(f"Found {len(playlist_data)} playlists") - - # List all tracks in each playlist + playlist_data = spotify.list("me/playlists", {"limit": 50}) for playlist in playlist_data: - print("Loading playlist: {name} ({tracks[total]} songs)".format(**playlist)) - playlist["tracks"] = spotify.list( - playlist["tracks"]["href"], {"limit": 100} - ) - playlists += playlist_data + print(f"Loading playlist: {playlist['name']}") + playlist["tracks"] = spotify.list(playlist["tracks"]["href"], {"limit": 100}) + playlists.extend(playlist_data) + + return playlists, liked_albums - # Write the file. - print("Writing files...") + +def write_to_file(file, format, playlists, liked_albums): + """Write fetched data to a file in the specified format.""" + print(f"Writing to {file}...") with open(file, "w", encoding="utf-8") as f: - # JSON file. if format == "json": json.dump({"playlists": playlists, "albums": liked_albums}, f) - - # Tab-separated file. else: - f.write("Playlists: \r\n\r\n") for playlist in playlists: f.write(playlist["name"] + "\r\n") for track in playlist["tracks"]: - if track["track"] is None: - continue - f.write( - "{name}\t{artists}\t{album}\t{uri}\t{release_date}\r\n".format( - uri=track["track"]["uri"], - name=track["track"]["name"], - artists=", ".join( - [artist["name"] for artist in track["track"]["artists"]] - ), - album=track["track"]["album"]["name"], - release_date=track["track"]["album"]["release_date"], + if track["track"]: + f.write( + "{name}\t{artists}\t{album}\t{uri}\t{release_date}\r\n".format( + uri=track["track"]["uri"], + name=track["track"]["name"], + artists=", ".join( + [artist["name"] for artist in track["track"]["artists"]] + ), + album=track["track"]["album"]["name"], + release_date=track["track"]["album"]["release_date"], + ) ) - ) f.write("\r\n") - if len(liked_albums) > 0: - f.write("Liked Albums: \r\n\r\n") - for album in liked_albums: - uri = album["album"]["uri"] - name = album["album"]["name"] - artists = ", ".join( - [artist["name"] for artist in album["album"]["artists"]] - ) - release_date = album["album"]["release_date"] - album = f"{artists} - {name}" - - f.write(f"{name}\t{artists}\t-\t{uri}\t{release_date}\r\n") - - print("Wrote file: " + file) + + +def main(dump="playlists,liked", format="json", file="playlists.json", token=""): + print("Starting backup...") + spotify = SpotifyAPI(token) if token else SpotifyAPI.authorize( + client_id="5c098bcc800e45d49e476265bc9b6934", + scope="playlist-read-private playlist-read-collaborative user-library-read", + ) + + playlists, liked_albums = fetch_user_data(spotify, dump) + write_to_file(file, format, playlists, liked_albums) + print(f"Backup completed! Data written to {file}") if __name__ == "__main__": diff --git a/spotify2ytmusic/ytmusic_credentials.py b/spotify2ytmusic/ytmusic_credentials.py new file mode 100644 index 0000000..2b9c848 --- /dev/null +++ b/spotify2ytmusic/ytmusic_credentials.py @@ -0,0 +1,43 @@ +import ytmusicapi + +import os + +def setup_ytmusic_with_raw_headers(input_file="raw_headers.txt", credentials_file="oauth.json"): + """ + Loads raw headers from a file and sets up YTMusic connection using ytmusicapi.setup. + + Parameters: + input_file (str): Path to the file containing raw headers. + credentials_file (str): Path to save the configuration headers (credentials). + + Returns: + str: Configuration headers string returned by ytmusicapi.setup. + """ + # Check if the input file exists + if not os.path.exists(input_file): + raise FileNotFoundError(f"Input file {input_file} does not exist.") + + # Read the raw headers from the file + with open(input_file, "r") as file: + headers_raw = file.read() + + # Use ytmusicapi.setup to process headers and save the credentials + config_headers = ytmusicapi.setup(filepath=credentials_file, headers_raw=headers_raw) + print(f"Configuration headers saved to {credentials_file}") + return config_headers + + +if __name__ == "__main__": + try: + # Specify file paths + raw_headers_file = "raw_headers.txt" + credentials_file = "oauth.json" + + # Set up YTMusic with raw headers + print(f"Setting up YTMusic using headers from {raw_headers_file}...") + setup_ytmusic_with_raw_headers(input_file=raw_headers_file, credentials_file=credentials_file) + + print("YTMusic setup completed successfully!") + + except Exception as e: + print(f"An error occurred: {e}")