Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Login Issue + Add Connection Stability Features + Avoid Ban Guide #117

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d8fc9cc
Update spotify_backup.py
CY83R-3X71NC710N Sep 26, 2024
930f2b7
Update README.md
CY83R-3X71NC710N Sep 27, 2024
fd6b770
Update README.md
CY83R-3X71NC710N Sep 27, 2024
1c7a677
Update README.md
CY83R-3X71NC710N Sep 27, 2024
a4c5406
Update README.md
CY83R-3X71NC710N Sep 27, 2024
f120c21
Update README.md
CY83R-3X71NC710N Sep 27, 2024
86db3fe
Update README.md
CY83R-3X71NC710N Sep 27, 2024
886dd95
Update README.md
CY83R-3X71NC710N Sep 27, 2024
74f2025
Update README.md
CY83R-3X71NC710N Sep 27, 2024
95e8e50
Update README.md
CY83R-3X71NC710N Sep 27, 2024
0ffaef4
Update README.md
CY83R-3X71NC710N Sep 27, 2024
a3f381f
Update README.md
CY83R-3X71NC710N Sep 27, 2024
af1e0f1
Update README.md
CY83R-3X71NC710N Sep 27, 2024
ae4e48f
Update README.md
CY83R-3X71NC710N Sep 27, 2024
5b2bef4
Update README.md
CY83R-3X71NC710N Sep 27, 2024
57cd779
Update README.md
CY83R-3X71NC710N Sep 27, 2024
5d641f9
Update README.md
CY83R-3X71NC710N Sep 27, 2024
3438396
Update README.md
CY83R-3X71NC710N Sep 27, 2024
d10fbe0
Update README.md
CY83R-3X71NC710N Sep 27, 2024
56aa579
Update README.md
CY83R-3X71NC710N Sep 27, 2024
eec8af8
Update README.md
CY83R-3X71NC710N Sep 27, 2024
cad9a38
Update README.md
CY83R-3X71NC710N Sep 27, 2024
fd3151f
Update backend.py
CY83R-3X71NC710N Sep 27, 2024
b9b33be
Update backend.py
CY83R-3X71NC710N Sep 27, 2024
e506624
Update README.md
CY83R-3X71NC710N Sep 27, 2024
e0b0467
Update backend.py
CY83R-3X71NC710N Sep 27, 2024
d2e18f7
Update backend.py
CY83R-3X71NC710N Sep 27, 2024
189723a
Update __main__.py
CY83R-3X71NC710N Sep 27, 2024
736a108
Update spotify_backup.py
CY83R-3X71NC710N Sep 27, 2024
d6ad06b
Update gui.py
CY83R-3X71NC710N Sep 27, 2024
5f91148
Update README.md
CY83R-3X71NC710N Sep 27, 2024
3495a22
Update backend.py
CY83R-3X71NC710N Sep 27, 2024
f2b905c
Update gui.py
CY83R-3X71NC710N Sep 27, 2024
178e709
Update README.md
CY83R-3X71NC710N Sep 27, 2024
17c6a49
Update README.md
CY83R-3X71NC710N Sep 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,40 @@
# Running:
```
brew install python
brew install python-tk
git clone https://github.com/CY83R-3X71NC710N/spotify_to_ytmusic.git && cd spotify_to_ytmusic
python3 -m venv path/to/venv
source path/to/venv/bin/activate
pip3 install ytmusicapi
ytmusicapi oauth
cd spotify2ytmusic && python3 spotify_backup.py playlists.json --dump=liked,playlists --format=json
mv playlists.json ..
cd .. && python3 ./reverse_playlist.py ./playlists.json -r
rm -rf log.txt
python -u -m spotify2ytmusic load_liked --track-sleep=3 --algo 0 2>&1 | tee -a log.txt && python -u -m spotify2ytmusic load_liked_albums --track-sleep=3 --algo 0 2>&1 | tee -a log.txt && python -u -m spotify2ytmusic copy_all_playlists --track-sleep=3 --algo 0 --privacy PRIVATE 2>&1 | tee -a log.txt
```

# For Highest Accuracy of songs:
Run this commnad

```
python -u -m spotify2ytmusic load_liked --track-sleep=3 --algo 0 2>&1 | tee -a log.txt && python -u -m spotify2ytmusic load_liked_albums --track-sleep=3 --algo 0 2>&1 | tee -a log.txt && python -u -m spotify2ytmusic copy_all_playlists --track-sleep=3 --algo 0 --privacy PRIVATE 2>&1 | tee -a log.txt
```

Three different times with every single algorithm 0,1,2

# Avoiding Ban:

Run the terminal application to transfer all playlists with a time to sleep of 3 seconds --track-sleep=3

# Connection Stability

Various retry functions were added to the backend to ensure that a faulty connection does not prevent songs from being transfered.

# Ideas
Look into creating a youtube to youtube version of this, which works off google takeout data (possibly create a script to backup youtube through github actions daily), and can restore data to a new youtube account.

# spotify_to_ytmusic offical README.md
Tools for moving from Spotify to YTMusic

# Overview
Expand Down
16 changes: 13 additions & 3 deletions spotify2ytmusic/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,26 @@

from . import cli
import sys
import inspect

def list_commands(module):
# include only functions defined in e.g. 'cli' module
commands = [name for name, obj in inspect.getmembers(module) if inspect.isfunction(obj)]
return commands

available_commands = list_commands(cli)

if len(sys.argv) < 2:
print(f"usage: spotify2ytmusic [COMMAND] <ARGUMENTS>")
print(" For example, try 'list_playlists'")
print("Available commands:", ", ".join(available_commands))
print(" For example, try 'spotify2ytmusic list_playlists'")
sys.exit(1)

if not hasattr(cli, sys.argv[1]):
if sys.argv[1] not in available_commands:
print(
f"ERROR: Unknown command, see https://github.com/linsomniac/spotify_to_ytmusic"
f"ERROR: Unknown command '{sys.argv[1]}', see https://github.com/linsomniac/spotify_to_ytmusic"
)
print("Available commands: ", ", ".join(available_commands))
sys.exit(1)

fn = getattr(cli, sys.argv[1])
Expand Down
90 changes: 57 additions & 33 deletions spotify2ytmusic/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,20 +355,21 @@ def copier(
yt: Optional[YTMusic] = None,
):
"""
@@@
Copies tracks from a source to a YouTube playlist, retrying indefinitely
if network issues occur during song lookup or playlist addition,
but limiting retries for specific errors like "track not found".
"""
if yt is None:
yt = get_ytmusic()

# Try to get playlist if specified
if dst_pl_id is not None:
try:
yt_pl = yt.get_playlist(playlistId=dst_pl_id)
except Exception as e:
print(f"ERROR: Unable to find YTMusic playlist {dst_pl_id}: {e}")
print(
" Make sure the YTMusic playlist ID is correct, it should be something like "
)
print(" 'PL_DhcdsaJ7echjfdsaJFhdsWUd73HJFca'")
print(" Make sure the YTMusic playlist ID is correct, it should be something like")
print(" 'PL_DhcdsaJ7echjfdsaJFhdsWUd73HJFca'")
sys.exit(1)
print(f"== Youtube Playlist: {yt_pl['title']}")

Expand All @@ -379,14 +380,31 @@ def copier(
for src_track in src_tracks:
print(f"Spotify: {src_track.title} - {src_track.artist} - {src_track.album}")

try:
dst_track = lookup_song(
yt, src_track.title, src_track.artist, src_track.album, yt_search_algo
)
except Exception as e:
print(f"ERROR: Unable to look up song on YTMusic: {e}")
error_count += 1
continue
# Retry song lookup indefinitely until successful
dst_track = None
retries = 0
max_retries = 3 # Limit for specific error retries
while dst_track is None:
try:
dst_track = lookup_song(
yt, src_track.title, src_track.artist, src_track.album, yt_search_algo
)
except Exception as e:
# Check if it's a specific error indicating the track was not found
if "list index out of range" in str(e):
retries += 1
if retries >= max_retries:
print(f"ERROR: Track not found after {max_retries} retries: {src_track.title}")
break # Stop retrying this track
else:
# If it's a different error, keep retrying indefinitely
print(f"ERROR: Unable to look up song on YTMusic: {e}, retrying in 5 seconds...")
time.sleep(5) # Delay before retrying
error_count += 1

# If dst_track is still None, it means we have exceeded max retries
if dst_track is None:
continue # Skip to the next track

yt_artist_name = "<Unknown>"
if "artists" in dst_track and len(dst_track["artists"]) > 0:
Expand All @@ -398,11 +416,14 @@ def copier(
if dst_track["videoId"] in tracks_added_set:
print("(DUPLICATE, this track has already been added)")
duplicate_count += 1
continue
tracks_added_set.add(dst_track["videoId"])

if not dry_run:
exception_sleep = 5
for _ in range(10):

# Retry playlist addition indefinitely until successful
while True:
try:
if dst_pl_id is not None:
yt.add_playlist_items(
Expand All @@ -412,23 +433,20 @@ def copier(
)
else:
yt.rate_song(dst_track["videoId"], "LIKE")
break
break # Break out of the loop on success
except Exception as e:
print(
f"ERROR: (Retrying add_playlist_items: {dst_pl_id} {dst_track['videoId']}) {e} in {exception_sleep} seconds"
f"ERROR: (Retrying add_playlist_items: {dst_pl_id} {dst_track['videoId']}) {e}, retrying in {exception_sleep} seconds"
)
time.sleep(exception_sleep)
exception_sleep *= 2
exception_sleep = min(exception_sleep * 2, 320) # Exponential backoff with max delay

if track_sleep:
time.sleep(track_sleep)

print()
print(
f"Added {len(tracks_added_set)} tracks, encountered {duplicate_count} duplicates, {error_count} errors"
)


print(f"Added {len(tracks_added_set)} tracks, encountered {duplicate_count} duplicates, {error_count} errors")

def copy_playlist(
spotify_playlist_id: str,
ytmusic_playlist_id: str,
Expand All @@ -453,26 +471,32 @@ def copy_playlist(
ytmusic_playlist_id = get_playlist_id_by_name(yt, pl_name)
print(f"Looking up playlist '{pl_name}': id={ytmusic_playlist_id}")

if ytmusic_playlist_id is None:
if not ytmusic_playlist_id or ytmusic_playlist_id is None:
if pl_name == "":
print("No playlist name or ID provided, creating playlist...")
spotify_pls: dict = load_playlists_json()
for pl in spotify_pls["playlists"]:
if len(pl.keys()) > 3 and pl["id"] == spotify_playlist_id:
pl_name = pl["name"]

ytmusic_playlist_id = _ytmusic_create_playlist(
yt,
title=pl_name,
description=pl_name,
privacy_status=privacy_status,
)
ytmusic_playlist_id = get_playlist_id_by_name(yt, pl_name)

# create_playlist returns a dict if there was an error
if isinstance(ytmusic_playlist_id, dict):
print(f"ERROR: Failed to create playlist: {ytmusic_playlist_id}")
sys.exit(1)
print(f"NOTE: Created playlist '{pl_name}' with ID: {ytmusic_playlist_id}")
if not ytmusic_playlist_id:
ytmusic_playlist_id = _ytmusic_create_playlist(
yt,
title=pl_name,
description=pl_name,
privacy_status=privacy_status,
)

# create_playlist returns a dict if there was an error
if isinstance(ytmusic_playlist_id, dict):
print(f"ERROR: Failed to create playlist: {ytmusic_playlist_id}")
sys.exit(1)
print(f"NOTE: Created playlist '{pl_name}' with ID: {ytmusic_playlist_id}")
else:
print(f"NOTE: '{pl_name}' already exists with ID: {ytmusic_playlist_id}. Appending...")

copier(
iter_spotify_playlist(
Expand Down
13 changes: 5 additions & 8 deletions spotify2ytmusic/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ def __init__(self) -> None:
command=lambda: self.call_func(
func=backend.copy_playlist,
args=(
self.spotify_playlist_id.get(),
self.spotify_playlist_id.get().strip(),
self.yt_playlist_id.get(),
"utf-8",
False,
Expand Down Expand Up @@ -320,16 +320,13 @@ def run_in_thread():
else: # For Unix and Linux
try:
subprocess.call(
"x-terminal-emulator -e ytmusicapi oauth",
shell=True,
stdout=subprocess.PIPE,
)
except:
subprocess.call(
"xterm -e ytmusicapi oauth",
"python3 -m ytmusicapi oauth",
shell=True,
stdout=subprocess.PIPE,
)
except Exception as e:
print(f"An error occurred: {e}")


self.tabControl.select(self.tab2)
print()
Expand Down
6 changes: 3 additions & 3 deletions spotify2ytmusic/spotify_backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ def main(dump="playlists,liked", format="json", file="playlists.json", token="")
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}
"me/tracks".format(user_id=user_id_escaped), {"limit": 50}
)
liked_albums = spotify.list("me/albums", {"limit": 50})
playlists += [{"name": "Liked Songs", "tracks": liked_tracks}]
Expand All @@ -162,7 +162,7 @@ def main(dump="playlists,liked", format="json", file="playlists.json", token="")
if "playlists" in dump:
print("Loading playlists...")
playlist_data = spotify.list(
"users/{user_id}/playlists".format(user_id=user_id_escaped), {"limit": 50}
"me/playlists".format(user_id=user_id_escaped), {"limit": 50}
)
print(f"Found {len(playlist_data)} playlists")

Expand All @@ -187,7 +187,7 @@ def main(dump="playlists,liked", format="json", file="playlists.json", token="")
for playlist in playlists:
f.write(playlist["name"] + "\r\n")
for track in playlist["tracks"]:
if track["track"] is None:
if track["track"] is None or track["track"]["type"] == "episode":
continue
f.write(
"{name}\t{artists}\t{album}\t{uri}\t{release_date}\r\n".format(
Expand Down