add featured artists to playlist page

+ fetch album bio on raising bottom container
This commit is contained in:
geoffrey45
2022-07-08 16:39:16 +03:00
parent 9d5cbfcc93
commit 09c588c856
15 changed files with 154 additions and 84 deletions
+7 -9
View File
@@ -64,8 +64,12 @@ def get_album():
except AttributeError: except AttributeError:
album.duration = 0 album.duration = 0
if (album.count == 1 and tracks[0].title == album.title if (
and tracks[0].tracknumber == 1 and tracks[0].disknumber == 1): album.count == 1
and tracks[0].title == album.title
and tracks[0].tracknumber == 1
and tracks[0].disknumber == 1
):
album.is_single = True album.is_single = True
return {"tracks": tracks, "info": album} return {"tracks": tracks, "info": album}
@@ -109,12 +113,6 @@ def get_albumartists():
if artist not in artists: if artist not in artists:
artists.append(artist) artists.append(artist)
final_artists = [] final_artists = [models.Artist(a) for a in artists]
for artist in artists:
artist_obj = {
"name": artist,
"image": helpers.create_safe_name(artist) + ".webp",
}
final_artists.append(artist_obj)
return {"artists": final_artists} return {"artists": final_artists}
+11 -10
View File
@@ -3,7 +3,6 @@ Contains all the playlist routes.
""" """
from datetime import datetime from datetime import datetime
from app import api
from app import exceptions from app import exceptions
from app import instances from app import instances
from app import models from app import models
@@ -17,8 +16,8 @@ from flask import request
playlist_bp = Blueprint("playlist", __name__, url_prefix="/") playlist_bp = Blueprint("playlist", __name__, url_prefix="/")
PlaylistExists = exceptions.PlaylistExists PlaylistExists = exceptions.PlaylistExistsError
TrackExistsInPlaylist = exceptions.TrackExistsInPlaylist TrackExistsInPlaylist = exceptions.TrackExistsInPlaylistError
@playlist_bp.route("/playlists", methods=["GET"]) @playlist_bp.route("/playlists", methods=["GET"])
@@ -28,8 +27,7 @@ def get_all_playlists():
dbplaylists = [models.Playlist(p) for p in dbplaylists] dbplaylists = [models.Playlist(p) for p in dbplaylists]
playlists = [ playlists = [
serializer.Playlist(p, construct_last_updated=False) serializer.Playlist(p, construct_last_updated=False) for p in dbplaylists
for p in dbplaylists
] ]
playlists.sort( playlists.sort(
key=lambda p: datetime.strptime(p.lastUpdated, "%Y-%m-%d %H:%M:%S"), key=lambda p: datetime.strptime(p.lastUpdated, "%Y-%m-%d %H:%M:%S"),
@@ -121,7 +119,6 @@ def update_playlist(playlistid: str):
image_, thumb_ = playlistlib.save_p_image(image, playlistid) image_, thumb_ = playlistlib.save_p_image(image, playlistid)
playlist["image"] = image_ playlist["image"] = image_
playlist["thumb"] = thumb_ playlist["thumb"] = thumb_
else: else:
playlist["image"] = p.image.split("/")[-1] playlist["image"] = p.image.split("/")[-1]
playlist["thumb"] = p.thumb.split("/")[-1] playlist["thumb"] = p.thumb.split("/")[-1]
@@ -136,7 +133,11 @@ def update_playlist(playlistid: str):
return {"msg": "Something shady happened"}, 500 return {"msg": "Something shady happened"}, 500
# @playlist_bp.route("/playlist/<playlist_id>/info") @playlist_bp.route("/playlist/artists", methods=["POST"])
# def get_playlist_track(playlist_id: str): def get_playlist_artists():
# tracks = playlistlib.get_playlist_tracks(playlist_id) data = request.get_json()
# return {"data": tracks}
pid = data["pid"]
artists = playlistlib.GetPlaylistArtists(pid)()
return {"data": artists}
+2 -2
View File
@@ -1,4 +1,4 @@
class TrackExistsInPlaylist(Exception): class TrackExistsInPlaylistError(Exception):
""" """
Exception raised when a track is already in a playlist. Exception raised when a track is already in a playlist.
""" """
@@ -6,7 +6,7 @@ class TrackExistsInPlaylist(Exception):
pass pass
class PlaylistExists(Exception): class PlaylistExistsError(Exception):
""" """
Exception raised when a playlist already exists. Exception raised when a playlist already exists.
""" """
+11 -15
View File
@@ -28,23 +28,18 @@ def run_checks():
Checks for new songs every 5 minutes. Checks for new songs every 5 minutes.
""" """
ValidateAlbumThumbs() ValidateAlbumThumbs()
ValidatePlaylistThumbs()
while True: while True:
trackslib.validate_tracks() trackslib.validate_tracks()
Populate() Populate()
CreateAlbums() CreateAlbums()
ProcessAlbumColors()
if helpers.Ping()(): if helpers.Ping()():
CheckArtistImages()() CheckArtistImages()()
@helpers.background
def process_album_colors():
ProcessAlbumColors()
ValidatePlaylistThumbs()
process_album_colors()
time.sleep(300) time.sleep(300)
@@ -79,7 +74,6 @@ class getArtistImage:
class useImageDownloader: class useImageDownloader:
def __init__(self, url: str, dest: str) -> None: def __init__(self, url: str, dest: str) -> None:
self.url = url self.url = url
self.dest = dest self.dest = dest
@@ -96,10 +90,8 @@ class useImageDownloader:
class CheckArtistImages: class CheckArtistImages:
def __init__(self): def __init__(self):
self.artists: list[str] = [] self.artists: list[str] = []
print("Checking for artist images")
log.info("Checking artist images") log.info("Checking artist images")
@staticmethod @staticmethod
@@ -121,8 +113,12 @@ class CheckArtistImages:
:param artistname: The artist name :param artistname: The artist name
""" """
img_path = (settings.APP_DIR + "/images/artists/" + img_path = (
helpers.create_safe_name(artistname) + ".webp") settings.APP_DIR
+ "/images/artists/"
+ helpers.create_safe_name(artistname)
+ ".webp"
)
if cls.check_if_exists(img_path): if cls.check_if_exists(img_path):
return "exists" return "exists"
@@ -149,7 +145,8 @@ def fetch_album_bio(title: str, albumartist: str) -> str | None:
Returns the album bio for a given album. Returns the album bio for a given album.
""" """
last_fm_url = "http://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key={}&artist={}&album={}&format=json".format( last_fm_url = "http://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key={}&artist={}&album={}&format=json".format(
settings.LAST_FM_API_KEY, albumartist, title) settings.LAST_FM_API_KEY, albumartist, title
)
try: try:
response = requests.get(last_fm_url) response = requests.get(last_fm_url)
@@ -158,8 +155,7 @@ def fetch_album_bio(title: str, albumartist: str) -> str | None:
return None return None
try: try:
bio = data["album"]["wiki"]["summary"].split( bio = data["album"]["wiki"]["summary"].split('<a href="https://www.last.fm/')[0]
'<a href="https://www.last.fm/')[0]
except KeyError: except KeyError:
bio = None bio = None
+9 -15
View File
@@ -4,13 +4,11 @@ This module contains mini functions for the server.
import os import os
import threading import threading
from datetime import datetime from datetime import datetime
from typing import Dict from typing import Dict, List, Set
from typing import List
from typing import Set
import requests import requests
from app import instances
from app import models from app import instances, models
def background(func): def background(func):
@@ -51,7 +49,6 @@ def run_fast_scandir(__dir: str, full=False) -> Dict[List[str], List[str]]:
class RemoveDuplicates: class RemoveDuplicates:
def __init__(self, tracklist: List[models.Track]) -> None: def __init__(self, tracklist: List[models.Track]) -> None:
self.tracklist = tracklist self.tracklist = tracklist
@@ -73,13 +70,12 @@ def is_valid_file(filename: str) -> bool:
return False return False
def create_album_hash(title: str, artist: str) -> str: def create_hash(*args: List[str]) -> str:
""" """
Creates a simple hash for an album Creates a simple hash for an album
""" """
lower = (title + artist).replace(" ", "").lower() string = "".join(a for a in args).replace(" ", "")
hash = "".join([i for i in lower if i.isalnum()]) return "".join([i for i in string if i.isalnum()]).lower()
return hash
def create_new_date(): def create_new_date():
@@ -92,7 +88,7 @@ def create_safe_name(name: str) -> str:
""" """
Creates a url-safe name from a name. Creates a url-safe name from a name.
""" """
return "".join([i for i in name if i.isalnum()]) return "".join([i for i in name if i.isalnum()]).lower()
class UseBisection: class UseBisection:
@@ -103,8 +99,7 @@ class UseBisection:
items. items.
""" """
def __init__(self, source: List, search_from: str, def __init__(self, source: List, search_from: str, queries: List[str]) -> None:
queries: List[str]) -> None:
self.source_list = source self.source_list = source
self.queries_list = queries self.queries_list = queries
self.attr = search_from self.attr = search_from
@@ -134,7 +129,6 @@ class UseBisection:
class Get: class Get:
@staticmethod @staticmethod
def get_all_tracks() -> List[models.Track]: def get_all_tracks() -> List[models.Track]:
""" """
@@ -157,7 +151,7 @@ class Get:
for track in tracks: for track in tracks:
for artist in track.artists: for artist in track.artists:
artists.add(artist.lower()) artists.add(artist)
return artists return artists
+25 -7
View File
@@ -18,7 +18,8 @@ from PIL import Image
from PIL import ImageSequence from PIL import ImageSequence
from werkzeug import datastructures from werkzeug import datastructures
TrackExistsInPlaylist = exceptions.TrackExistsInPlaylist
TrackExistsInPlaylist = exceptions.TrackExistsInPlaylistError
logg = get_logger() logg = get_logger()
@@ -52,8 +53,7 @@ def create_thumbnail(image: any, img_path: str) -> str:
Creates a 250 x 250 thumbnail from a playlist image Creates a 250 x 250 thumbnail from a playlist image
""" """
thumb_path = "thumb_" + img_path thumb_path = "thumb_" + img_path
full_thumb_path = os.path.join(settings.APP_DIR, "images", "playlists", full_thumb_path = os.path.join(settings.APP_DIR, "images", "playlists", thumb_path)
thumb_path)
aspect_ratio = image.width / image.height aspect_ratio = image.width / image.height
@@ -71,13 +71,11 @@ def save_p_image(file: datastructures.FileStorage, pid: str):
""" """
img = Image.open(file) img = Image.open(file)
random_str = "".join( random_str = "".join(random.choices(string.ascii_letters + string.digits, k=5))
random.choices(string.ascii_letters + string.digits, k=5))
img_path = pid + str(random_str) + ".webp" img_path = pid + str(random_str) + ".webp"
full_img_path = os.path.join(settings.APP_DIR, "images", "playlists", full_img_path = os.path.join(settings.APP_DIR, "images", "playlists", img_path)
img_path)
if file.content_type == "image/gif": if file.content_type == "image/gif":
frames = [] frames = []
@@ -140,3 +138,23 @@ def create_playlist_tracks(playlist_tracks: List) -> List[models.Track]:
tracks.append(models.Track(track)) tracks.append(models.Track(track))
return tracks return tracks
class GetPlaylistArtists:
"""
Returns a list of artists from a list of playlist tracks.
"""
def __init__(self, pid: str) -> None:
self.pid = pid
p = instances.playlist_instance.get_playlist_by_id(self.pid)
self.tracks = create_playlist_tracks(p["pre_tracks"])
def __call__(self):
artists = set()
for t in self.tracks:
for a in t.artists:
artists.add(a)
return [models.Artist(a) for a in artists]
+2 -2
View File
@@ -5,7 +5,7 @@ from typing import List
from app import instances from app import instances
from app import settings from app import settings
from app.helpers import create_album_hash from app.helpers import create_hash
from app.helpers import Get from app.helpers import Get
from app.helpers import run_fast_scandir from app.helpers import run_fast_scandir
from app.helpers import UseBisection from app.helpers import UseBisection
@@ -51,7 +51,7 @@ class Populate:
tags = get_tags(file) tags = get_tags(file)
if tags is not None: if tags is not None:
hash = create_album_hash(tags["album"], tags["albumartist"]) hash = create_hash(tags["album"], tags["albumartist"])
tags["albumhash"] = hash tags["albumhash"] = hash
self.tagged_tracks.append(tags) self.tagged_tracks.append(tags)
+3 -6
View File
@@ -5,7 +5,7 @@ import os
import time import time
from app import instances from app import instances
from app.helpers import create_album_hash from app.helpers import create_hash
from app.lib.taglib import get_tags from app.lib.taglib import get_tags
from app.logger import get_logger from app.logger import get_logger
from watchdog.events import PatternMatchingEventHandler from watchdog.events import PatternMatchingEventHandler
@@ -53,7 +53,7 @@ def add_track(filepath: str) -> None:
tags = get_tags(filepath) tags = get_tags(filepath)
if tags is not None: if tags is not None:
hash = create_album_hash(tags["album"], tags["albumartist"]) hash = create_hash(tags["album"], tags["albumartist"])
tags["albumhash"] = hash tags["albumhash"] = hash
instances.tracks_instance.insert_song(tags) instances.tracks_instance.insert_song(tags)
@@ -82,21 +82,19 @@ class Handler(PatternMatchingEventHandler):
""" """
Fired when a supported file is created. Fired when a supported file is created.
""" """
print("🔵 created +++")
self.files_to_process.append(event.src_path) self.files_to_process.append(event.src_path)
def on_deleted(self, event): def on_deleted(self, event):
""" """
Fired when a delete event occurs on a supported file. Fired when a delete event occurs on a supported file.
""" """
print("🔴 deleted ---")
remove_track(event.src_path) remove_track(event.src_path)
def on_moved(self, event): def on_moved(self, event):
""" """
Fired when a move event occurs on a supported file. Fired when a move event occurs on a supported file.
""" """
print("🔘 moved -->")
tr = "share/Trash" tr = "share/Trash"
if tr in event.dest_path: if tr in event.dest_path:
@@ -114,7 +112,6 @@ class Handler(PatternMatchingEventHandler):
""" """
Fired when a created file is closed. Fired when a created file is closed.
""" """
print("⚫ closed ~~~")
try: try:
self.files_to_process.remove(event.src_path) self.files_to_process.remove(event.src_path)
add_track(event.src_path) add_track(event.src_path)
+6 -7
View File
@@ -49,8 +49,9 @@ class Track:
self.image = tags["albumhash"] + ".webp" self.image = tags["albumhash"] + ".webp"
self.tracknumber = int(tags["tracknumber"]) self.tracknumber = int(tags["tracknumber"])
self.uniq_hash = self.create_unique_hash("".join(self.artists), self.uniq_hash = helpers.create_hash(
self.album, self.title) "".join(self.artists), self.album, self.title
)
@staticmethod @staticmethod
def create_unique_hash(*args): def create_unique_hash(*args):
@@ -64,14 +65,12 @@ class Artist:
Artist class Artist class
""" """
artistid: str
name: str name: str
image: str image: str
def __init__(self, tags): def __init__(self, name: str):
self.artistid = tags["_id"]["$oid"] self.name = name
self.name = tags["name"] self.image = helpers.create_safe_name(name) + ".webp"
self.image = tags["image"]
@dataclass @dataclass
+27
View File
@@ -1,3 +1,4 @@
import { Artist } from "./../../interfaces";
import { Playlist, Track } from "../../interfaces"; import { Playlist, Track } from "../../interfaces";
import { Notification, NotifType } from "../../stores/notification"; import { Notification, NotifType } from "../../stores/notification";
import state from "../state"; import state from "../state";
@@ -120,6 +121,32 @@ async function updatePlaylist(pid: string, playlist: FormData, pStore: any) {
} }
} }
/**
* Gets the artists in a playlist.
* @param pid The playlist id to fetch tracks for.
* @returns {Promise<Artist[]>} A promise that resolves to an array of artists.
*/
export async function getPlaylistArtists(pid: string): Promise<Artist[]> {
const uri = state.settings.uri + "/playlist/artists";
const { data, error } = await useAxios({
url: uri,
props: {
pid: pid,
},
});
if (error) {
new Notification("Something funny happened!", NotifType.Error);
}
if (data) {
return data.data as Artist[];
}
return [];
}
export { export {
createNewPlaylist, createNewPlaylist,
getAllPlaylists, getAllPlaylists,
@@ -28,14 +28,23 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from "vue"; import { onMounted, ref } from "vue";
import useVisibility from "../../composables/useVisibility"; import useVisibility from "@/composables/useVisibility";
import useNavStore from "../../stores/nav"; import useNavStore from "@/stores/nav";
import { onBeforeRouteUpdate, RouteParams, useRoute } from "vue-router";
const nav = useNavStore(); const nav = useNavStore();
const props = defineProps<{
/**
* Called when the bottom container is raised.
*/
onBottomRaised?: (routeparams?: RouteParams) => void;
}>();
let elem: HTMLElement = null; let elem: HTMLElement = null;
let classlist: DOMTokenList = null; let classlist: DOMTokenList = null;
const route = useRoute();
const apheader = ref<HTMLElement>(null); const apheader = ref<HTMLElement>(null);
const apbottomcontainer = ref(null); const apbottomcontainer = ref(null);
const bottomContainerRaised = ref(false); const bottomContainerRaised = ref(false);
@@ -45,6 +54,12 @@ onMounted(() => {
classlist = elem.classList; classlist = elem.classList;
}); });
onBeforeRouteUpdate((to) => {
if (bottomContainerRaised.value) {
props.onBottomRaised(to.params);
}
});
function handleVisibilityState(state: boolean) { function handleVisibilityState(state: boolean) {
resetBottomPadding(); resetBottomPadding();
@@ -59,14 +74,19 @@ function resetBottomPadding() {
classlist.remove("addbottompadding"); classlist.remove("addbottompadding");
} }
let bottomRaisedCallbackExecuted = false;
function toggleBottom() { function toggleBottom() {
bottomContainerRaised.value = !bottomContainerRaised.value; bottomContainerRaised.value = !bottomContainerRaised.value;
if (bottomContainerRaised.value) { if (bottomContainerRaised.value) {
classlist.add("addbottompadding"); classlist.add("addbottompadding");
if (!bottomRaisedCallbackExecuted) {
bottomRaisedCallbackExecuted = true;
props.onBottomRaised(route.params);
}
return; return;
} }
if (elem.scrollTop == 0) { if (elem.scrollTop == 0) {
classlist.remove("addbottompadding"); classlist.remove("addbottompadding");
} }
-1
View File
@@ -66,7 +66,6 @@ const routes = [
state.loading.value = true; state.loading.value = true;
await useAStore().fetchTracksAndArtists(to.params.hash); await useAStore().fetchTracksAndArtists(to.params.hash);
state.loading.value = false; state.loading.value = false;
useAStore().fetchBio(to.params.hash);
}, },
}, },
{ {
+10 -1
View File
@@ -1,11 +1,16 @@
import { Artist } from "./../../interfaces";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { getPlaylist } from "../../composables/pages/playlists"; import {
getPlaylist,
getPlaylistArtists,
} from "../../composables/pages/playlists";
import { Track, Playlist } from "../../interfaces"; import { Track, Playlist } from "../../interfaces";
export default defineStore("playlist-tracks", { export default defineStore("playlist-tracks", {
state: () => ({ state: () => ({
info: <Playlist>{}, info: <Playlist>{},
tracks: <Track[]>[], tracks: <Track[]>[],
artists: <Artist[]>[],
}), }),
actions: { actions: {
/** /**
@@ -18,6 +23,10 @@ export default defineStore("playlist-tracks", {
this.info = playlist.info; this.info = playlist.info;
this.tracks = playlist.tracks; this.tracks = playlist.tracks;
}, },
async fetchArtists(playlistid: string) {
this.artists = await getPlaylistArtists(playlistid);
},
/** /**
* Updates the playlist header info. This is used when the playlist is * Updates the playlist header info. This is used when the playlist is
* updated. * updated.
+8 -4
View File
@@ -1,5 +1,5 @@
<template> <template>
<Page> <Page :onBottomRaised="fetchAlbumBio">
<template #header> <template #header>
<Header :album="album.info" /> <Header :album="album.info" />
</template> </template>
@@ -13,18 +13,22 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeRouteUpdate, RouteLocationNormalized } from "vue-router"; import { onBeforeRouteUpdate, RouteLocationNormalized, RouteParams } from "vue-router";
import useAStore from "@/stores/pages/album"; import useAStore from "@/stores/pages/album";
import Page from "../layouts/HeaderContentBottom.vue"; import Page from "@/layouts/HeaderContentBottom.vue";
import Header from "./Header.vue"; import Header from "./Header.vue";
import Content from "./Content.vue"; import Content from "./Content.vue";
import Bottom from "./Bottom.vue"; import Bottom from "./Bottom.vue";
import { onBeforeUnmount } from "vue";
const album = useAStore(); const album = useAStore();
function fetchAlbumBio(params: RouteParams) {
album.fetchBio(params.hash.toString());
}
onBeforeRouteUpdate(async (to: RouteLocationNormalized) => { onBeforeRouteUpdate(async (to: RouteLocationNormalized) => {
await album.fetchTracksAndArtists(to.params.hash.toString()); await album.fetchTracksAndArtists(to.params.hash.toString());
album.fetchBio(to.params.hash.toString());
}); });
</script> </script>
+10 -2
View File
@@ -12,21 +12,29 @@
/> />
</template> </template>
<template #bottom> <template #bottom>
<FeaturedArtists :artists="[]" /> <FeaturedArtists :artists="playlist.artists" />
</template> </template>
</Page> </Page>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Page from "../layouts/HeaderContentBottom.vue"; import Page from "@/layouts/HeaderContentBottom.vue";
import Header from "@/components/PlaylistView/Header.vue"; import Header from "@/components/PlaylistView/Header.vue";
import Content from "./Content.vue"; import Content from "./Content.vue";
import FeaturedArtists from "@/components/PlaylistView/FeaturedArtists.vue"; import FeaturedArtists from "@/components/PlaylistView/FeaturedArtists.vue";
import usePTrackStore from "@/stores/pages/playlist"; import usePTrackStore from "@/stores/pages/playlist";
import { onBeforeUnmount, onMounted } from "vue";
import { useRoute } from "vue-router";
const route = useRoute();
const playlist = usePTrackStore(); const playlist = usePTrackStore();
onMounted(() => {
playlist.fetchArtists(route.params.pid as string);
});
</script> </script>
<style lang="scss"></style> <style lang="scss"></style>