mirror of
				https://github.com/Alexander-D-Karpov/akarpov
				synced 2025-11-04 11:17:24 +03:00 
			
		
		
		
	Refactor music service and add Spotify support
This commit is contained in:
		
							parent
							
								
									0189377aeb
								
							
						
					
					
						commit
						db72084d64
					
				| 
						 | 
					@ -63,7 +63,7 @@ def filter(self, queryset):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if search_type in search_classes:
 | 
					            if search_type in search_classes:
 | 
				
			||||||
                search_instance = search_classes[search_type](
 | 
					                search_instance = search_classes[search_type](
 | 
				
			||||||
                    queryset=File.objects.filter(user=self.request.user)
 | 
					                    queryset=File.objects.filter(user=self.request.user).nocache()
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                queryset = search_instance.search(query)
 | 
					                queryset = search_instance.search(query)
 | 
				
			||||||
        return queryset
 | 
					        return queryset
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,11 @@
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import re
 | 
					import re
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import requests
 | 
				
			||||||
from deep_translator import GoogleTranslator
 | 
					from deep_translator import GoogleTranslator
 | 
				
			||||||
from django.core.files import File
 | 
					from django.core.files import File
 | 
				
			||||||
from django.db import transaction
 | 
					from django.db import transaction
 | 
				
			||||||
 | 
					from django.db.models import Min
 | 
				
			||||||
from django.utils.text import slugify
 | 
					from django.utils.text import slugify
 | 
				
			||||||
from mutagen import File as MutagenFile
 | 
					from mutagen import File as MutagenFile
 | 
				
			||||||
from mutagen.id3 import APIC, ID3, TCON, TORY, TextFrame
 | 
					from mutagen.id3 import APIC, ID3, TCON, TORY, TextFrame
 | 
				
			||||||
| 
						 | 
					@ -16,6 +18,32 @@
 | 
				
			||||||
from akarpov.users.models import User
 | 
					from akarpov.users.models import User
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_or_create_author(author_name):
 | 
				
			||||||
 | 
					    retry = True
 | 
				
			||||||
 | 
					    while retry:
 | 
				
			||||||
 | 
					        retry = False
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            with transaction.atomic():
 | 
				
			||||||
 | 
					                author, created = Author.objects.get_or_create(
 | 
				
			||||||
 | 
					                    name__iexact=author_name, defaults={"name": author_name}
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            return author
 | 
				
			||||||
 | 
					        except Author.MultipleObjectsReturned:
 | 
				
			||||||
 | 
					            with transaction.atomic():
 | 
				
			||||||
 | 
					                # If multiple authors are found, get the first one and delete the rest
 | 
				
			||||||
 | 
					                min_id = Author.objects.filter(name__iexact=author_name).aggregate(
 | 
				
			||||||
 | 
					                    Min("id")
 | 
				
			||||||
 | 
					                )["id__min"]
 | 
				
			||||||
 | 
					                author = Author.objects.get(id=min_id)
 | 
				
			||||||
 | 
					                Author.objects.filter(name__iexact=author_name).exclude(
 | 
				
			||||||
 | 
					                    id=min_id
 | 
				
			||||||
 | 
					                ).delete()
 | 
				
			||||||
 | 
					            return author
 | 
				
			||||||
 | 
					        except Exception as e:
 | 
				
			||||||
 | 
					            if "could not serialize access due to concurrent update" in str(e):
 | 
				
			||||||
 | 
					                retry = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def process_track_name(track_name: str) -> str:
 | 
					def process_track_name(track_name: str) -> str:
 | 
				
			||||||
    # Split the track name by dash and parentheses
 | 
					    # Split the track name by dash and parentheses
 | 
				
			||||||
    parts = track_name.split(" - ")
 | 
					    parts = track_name.split(" - ")
 | 
				
			||||||
| 
						 | 
					@ -78,23 +106,12 @@ def load_track(
 | 
				
			||||||
    if album and type(album) is str and album.startswith("['"):
 | 
					    if album and type(album) is str and album.startswith("['"):
 | 
				
			||||||
        album = album.replace("['", "").replace("']", "")
 | 
					        album = album.replace("['", "").replace("']", "")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    re_authors = []
 | 
					    processed_authors = []
 | 
				
			||||||
    if authors:
 | 
					    if authors:
 | 
				
			||||||
        for x in authors:
 | 
					        for author_name in authors:
 | 
				
			||||||
            while True:
 | 
					            author = get_or_create_author(author_name)
 | 
				
			||||||
                try:
 | 
					            processed_authors.append(author)
 | 
				
			||||||
                    with transaction.atomic():
 | 
					    authors = processed_authors
 | 
				
			||||||
                        author, created = Author.objects.get_or_create(
 | 
					 | 
				
			||||||
                            name__iexact=x, defaults={"name": x}
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                    re_authors.append(author)
 | 
					 | 
				
			||||||
                    break
 | 
					 | 
				
			||||||
                except Author.MultipleObjectsReturned:
 | 
					 | 
				
			||||||
                    # If multiple authors are found, delete all but one
 | 
					 | 
				
			||||||
                    Author.objects.filter(name__iexact=x).exclude(
 | 
					 | 
				
			||||||
                        id=Author.objects.filter(name__iexact=x).first().id
 | 
					 | 
				
			||||||
                    ).delete()
 | 
					 | 
				
			||||||
    authors = re_authors
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if album:
 | 
					    if album:
 | 
				
			||||||
        if type(album) is str:
 | 
					        if type(album) is str:
 | 
				
			||||||
| 
						 | 
					@ -122,6 +139,14 @@ def load_track(
 | 
				
			||||||
        path = mp3_path
 | 
					        path = mp3_path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    tag = MP3(path, ID3=ID3)
 | 
					    tag = MP3(path, ID3=ID3)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if image_path and image_path.startswith("http"):
 | 
				
			||||||
 | 
					        response = requests.get(image_path)
 | 
				
			||||||
 | 
					        se = image_path.split("/")[-1]
 | 
				
			||||||
 | 
					        image_path = f'/tmp/{generate_readable_slug(name, Song)}.{"png" if "." not in se else se.split(".")[-1]}'
 | 
				
			||||||
 | 
					        with open(image_path, "wb") as f:
 | 
				
			||||||
 | 
					            f.write(response.content)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if image_path:
 | 
					    if image_path:
 | 
				
			||||||
        if not image_path.endswith(".png"):
 | 
					        if not image_path.endswith(".png"):
 | 
				
			||||||
            nm = image_path
 | 
					            nm = image_path
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,10 @@
 | 
				
			||||||
import spotipy
 | 
					import spotipy
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					from spotdl import Song, Spotdl
 | 
				
			||||||
from spotipy.oauth2 import SpotifyClientCredentials
 | 
					from spotipy.oauth2 import SpotifyClientCredentials
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from akarpov.music.services.db import load_track
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def create_session() -> spotipy.Spotify:
 | 
					def create_session() -> spotipy.Spotify:
 | 
				
			||||||
    if not settings.MUSIC_SPOTIFY_ID or not settings.MUSIC_SPOTIFY_SECRET:
 | 
					    if not settings.MUSIC_SPOTIFY_ID or not settings.MUSIC_SPOTIFY_SECRET:
 | 
				
			||||||
| 
						 | 
					@ -18,3 +21,62 @@ def create_session() -> spotipy.Spotify:
 | 
				
			||||||
def search(name: str, session: spotipy.Spotify, search_type="track"):
 | 
					def search(name: str, session: spotipy.Spotify, search_type="track"):
 | 
				
			||||||
    res = session.search(name, type=search_type)
 | 
					    res = session.search(name, type=search_type)
 | 
				
			||||||
    return res
 | 
					    return res
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def download_url(url, user_id=None):
 | 
				
			||||||
 | 
					    spot_settings = {
 | 
				
			||||||
 | 
					        "simple_tui": True,
 | 
				
			||||||
 | 
					        "log_level": "DEBUG",
 | 
				
			||||||
 | 
					        "lyrics_providers": ["genius", "musixmatch"],
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    spotdl_client = Spotdl(
 | 
				
			||||||
 | 
					        client_id=settings.MUSIC_SPOTIFY_ID,
 | 
				
			||||||
 | 
					        client_secret=settings.MUSIC_SPOTIFY_SECRET,
 | 
				
			||||||
 | 
					        user_auth=False,
 | 
				
			||||||
 | 
					        cache_path="/tmp/",
 | 
				
			||||||
 | 
					        headless=False,
 | 
				
			||||||
 | 
					        downloader_settings=spot_settings,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    session = create_session()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if "track" in url:
 | 
				
			||||||
 | 
					        songs = [Song.from_url(url)]
 | 
				
			||||||
 | 
					    elif "album" in url:
 | 
				
			||||||
 | 
					        album_tracks = session.album(url)["tracks"]["items"]
 | 
				
			||||||
 | 
					        songs = [
 | 
				
			||||||
 | 
					            Song.from_url(track["external_urls"]["spotify"]) for track in album_tracks
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					    elif "artist" in url:
 | 
				
			||||||
 | 
					        artist_top_tracks = session.artist_top_tracks(url)["tracks"]
 | 
				
			||||||
 | 
					        songs = [
 | 
				
			||||||
 | 
					            Song.from_url(track["external_urls"]["spotify"])
 | 
				
			||||||
 | 
					            for track in artist_top_tracks
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					    elif "playlist" in url:
 | 
				
			||||||
 | 
					        playlist_tracks = session.playlist_items(url)["items"]
 | 
				
			||||||
 | 
					        songs = [
 | 
				
			||||||
 | 
					            Song.from_url(track["track"]["external_urls"]["spotify"])
 | 
				
			||||||
 | 
					            for track in playlist_tracks
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for song in songs:
 | 
				
			||||||
 | 
					        res = spotdl_client.download(song)
 | 
				
			||||||
 | 
					        if res:
 | 
				
			||||||
 | 
					            song, path = res
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					        load_track(
 | 
				
			||||||
 | 
					            path=str(path),
 | 
				
			||||||
 | 
					            image_path=song.cover_url,
 | 
				
			||||||
 | 
					            user_id=user_id,
 | 
				
			||||||
 | 
					            authors=song.artists,
 | 
				
			||||||
 | 
					            album=song.album_name,
 | 
				
			||||||
 | 
					            name=song.name,
 | 
				
			||||||
 | 
					            link=song.url,
 | 
				
			||||||
 | 
					            genre=song.genres[0] if song.genres else None,
 | 
				
			||||||
 | 
					            release=song.date,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,7 +19,7 @@
 | 
				
			||||||
    UserListenHistory,
 | 
					    UserListenHistory,
 | 
				
			||||||
    UserMusicProfile,
 | 
					    UserMusicProfile,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from akarpov.music.services import yandex, youtube
 | 
					from akarpov.music.services import spotify, yandex, youtube
 | 
				
			||||||
from akarpov.music.services.file import load_dir, load_file
 | 
					from akarpov.music.services.file import load_dir, load_file
 | 
				
			||||||
from akarpov.utils.celery import get_scheduled_tasks_name
 | 
					from akarpov.utils.celery import get_scheduled_tasks_name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,7 +28,11 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@shared_task
 | 
					@shared_task
 | 
				
			||||||
def list_tracks(url, user_id):
 | 
					def list_tracks(url, user_id):
 | 
				
			||||||
    if "music.yandex.ru" in url:
 | 
					    if "music.youtube.com" in url:
 | 
				
			||||||
 | 
					        url = url.replace("music.youtube.com", "youtube.com")
 | 
				
			||||||
 | 
					    if "spotify.com" in url:
 | 
				
			||||||
 | 
					        spotify.download_url(url, user_id)
 | 
				
			||||||
 | 
					    elif "music.yandex.ru" in url:
 | 
				
			||||||
        yandex.load_playlist(url, user_id)
 | 
					        yandex.load_playlist(url, user_id)
 | 
				
			||||||
    elif "channel" in url or "/c/" in url:
 | 
					    elif "channel" in url or "/c/" in url:
 | 
				
			||||||
        p = Channel(url)
 | 
					        p = Channel(url)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user