Skip to content

Commit 4e9e11f

Browse files
committed
implement a program
1 parent 19d56fb commit 4e9e11f

File tree

7 files changed

+164
-0
lines changed

7 files changed

+164
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,5 @@ cython_debug/
158158
# and can be added to the global gitignore or merged into this file. For a more nuclear
159159
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
160160
#.idea/
161+
162+
*.json

core/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .yandex import YandexMusicExporter
2+
from .youtube import YoutubeImoirter
3+
from .track import Track

core/track.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from typing import NamedTuple
2+
3+
4+
class Track(NamedTuple):
5+
artist: str
6+
name: str

core/yandex.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from yandex_music import Client, Artist
2+
from typing import List
3+
from .track import Track
4+
from tqdm import tqdm
5+
6+
7+
class YandexMusicExporter:
8+
def __init__(self, token: str):
9+
self.client = Client(token).init()
10+
11+
def export_liked_tracks(self) -> List[Track]:
12+
tracks = self.client.users_likes_tracks().tracks
13+
14+
result = []
15+
with tqdm(total=len(tracks), position=0, desc='Export tracks') as pbar:
16+
with tqdm(total=0, bar_format='{desc}', position=1) as trank_log:
17+
for track in tracks:
18+
track = track.fetch_track()
19+
artist = track.artists_name()[0]
20+
name = track.title
21+
result.append(Track(artist, name))
22+
pbar.update(1)
23+
trank_log.set_description_str(f'{artist} - {name}')
24+
return result

core/youtube.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from ytmusicapi import YTMusic, setup_oauth
2+
from .track import Track
3+
from typing import List, Tuple
4+
import os
5+
from tqdm import tqdm
6+
7+
8+
class YoutubeImoirter:
9+
def __init__(self, token_path: str):
10+
if not os.path.exists(token_path):
11+
token = setup_oauth().as_json()
12+
with open(token_path, 'w') as f:
13+
f.write(token)
14+
else:
15+
token = open(token_path, 'r').read()
16+
17+
self.ytmusic = YTMusic(token)
18+
19+
def import_liked_tracks(self, tracks: List[Track]) -> Tuple[List[Track], List[Track]]:
20+
not_found = []
21+
errors = []
22+
with tqdm(total=len(tracks), position=0, desc='Import tracks') as pbar:
23+
with tqdm(total=0, bar_format='{desc}', position=1) as trank_log:
24+
for track in tracks:
25+
results = self.ytmusic.search(f'{track.artist} {track.name}')
26+
if len(results) == 0:
27+
not_found.append(track)
28+
continue
29+
30+
result = self._get_best_result(results, track)
31+
try:
32+
self.ytmusic.rate_song(result['videoId'], 'LIKE')
33+
except Exception as e:
34+
errors.append(track)
35+
pbar.write(f'Error: {track.artist} - {track.name}, {e}')
36+
pbar.update(1)
37+
trank_log.set_description_str(f'{track.artist} - {track.name}')
38+
39+
return not_found, errors
40+
41+
def _get_best_result(self, results: List[dict], track: Track) -> dict:
42+
songs = []
43+
for result in results:
44+
if 'videoId' not in result.keys():
45+
continue
46+
if result['category'] == 'Top result':
47+
return result
48+
if result['title'] == track.name:
49+
return result
50+
songs.append(result)
51+
if len(songs) == 0:
52+
return results[0]
53+
return songs[0]
54+

main.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import argparse
2+
from core import YandexMusicExporter
3+
from core import YoutubeImoirter
4+
import json
5+
6+
7+
def parse_args() -> argparse.Namespace:
8+
parser = argparse.ArgumentParser(description='Train a model')
9+
parser.add_argument(
10+
'--yandex', type=str, help='Yandex Music token'
11+
)
12+
parser.add_argument(
13+
'--output', type=str, default='tracks.json', help='Output json file'
14+
)
15+
parser.add_argument(
16+
'--youtube', type=str, default='youtube.json',
17+
help='Youtube Music credentials file. If file not exists, it will be created.'
18+
)
19+
return parser.parse_args()
20+
21+
22+
def move_tracks(
23+
importer: YandexMusicExporter, exporter: YoutubeImoirter, out_path: str
24+
) -> None:
25+
data = {
26+
'liked_tracks': [],
27+
'not_found': [],
28+
'errors': [],
29+
}
30+
31+
print('Exporting liked tracks from Yandex Music...')
32+
tracks = importer.export_liked_tracks()
33+
tracks.reverse()
34+
35+
for track in tracks:
36+
data['liked_tracks'].append({
37+
'artist': track.artist,
38+
'name': track.name
39+
})
40+
41+
print('Importing liked tracks to Youtube Music...')
42+
not_found, errors = exporter.import_liked_tracks(tracks)
43+
44+
for track in not_found:
45+
data['not_found'].append({
46+
'artist': track.artist,
47+
'name': track.name
48+
})
49+
print(f'{track.artist} - {track.name}')
50+
51+
for track in errors:
52+
data['errors'].append({
53+
'artist': track.artist,
54+
'name': track.name
55+
})
56+
57+
print(f'{len(not_found)} not found tracks, {len(errors)} errors.')
58+
59+
str_data = json.dumps(data)
60+
with open(out_path, 'w', encoding='utf-8') as f:
61+
f.write(str_data)
62+
63+
64+
def main() -> None:
65+
args = parse_args()
66+
importer = YandexMusicExporter(args.yandex)
67+
exporter = YoutubeImoirter(args.youtube)
68+
move_tracks(importer, exporter, args.output)
69+
70+
71+
if __name__ == '__main__':
72+
main()

requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
yandex-music>=2.0.0
2+
ytmusicapi>=1.4.2
3+
tqdm

0 commit comments

Comments
 (0)