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