From 26b7cd376db1a9ab906caf208aac04142e43dead Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Sun, 26 Jun 2022 19:37:48 +0300 Subject: [PATCH 01/22] show track number as index in album page --- src/components/FolderView/SongList.vue | 23 +++++++++++++++++++++-- src/interfaces.ts | 1 + src/views/AlbumView.vue | 2 +- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/components/FolderView/SongList.vue b/src/components/FolderView/SongList.vue index e5fb9e65..9a5d95ac 100644 --- a/src/components/FolderView/SongList.vue +++ b/src/components/FolderView/SongList.vue @@ -10,10 +10,10 @@
(); let route = useRoute().name; @@ -68,6 +69,24 @@ function updateQueue(track: Track) { break; } } + +function getTracks() { + if (props.on_album_page) { + let tracks = props.tracks.map((track, index) => { + track.index = track.tracknumber; + return track; + }); + + return tracks; + } + + let tracks = props.tracks.map((track, index) => { + track.index = index + 1; + return track; + }); + + return tracks; +} \ No newline at end of file + diff --git a/src/components/AlbumView/Header.vue b/src/components/AlbumView/Header.vue index 22008810..5c41c3e0 100644 --- a/src/components/AlbumView/Header.vue +++ b/src/components/AlbumView/Header.vue @@ -53,18 +53,17 @@ const imguri = paths.images.thumb; const nav = useNavStore(); useVisibility(albumheaderthing, nav.toggleShowPlay); - - From a23b6200eb8ef6fee1d733001df3213275082833 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Thu, 30 Jun 2022 14:01:48 +0300 Subject: [PATCH 04/22] add colors to album page header - add colors attribute to the album class - render color gradient in the album page --- server/app/db/mongodb/albums.py | 10 ++++++ server/app/functions.py | 20 ++++++----- server/app/lib/colorlib.py | 53 ++++++++++++++--------------- server/app/models.py | 8 +++++ src/components/AlbumView/Header.vue | 14 ++++++-- src/interfaces.ts | 6 ++-- src/views/AlbumView.vue | 1 - 7 files changed, 70 insertions(+), 42 deletions(-) diff --git a/server/app/db/mongodb/albums.py b/server/app/db/mongodb/albums.py index 030c900c..4999c146 100644 --- a/server/app/db/mongodb/albums.py +++ b/server/app/db/mongodb/albums.py @@ -2,6 +2,7 @@ This file contains the Album class for interacting with album documents in MongoDB. """ +from typing import List from app.db.mongodb import convert_many from app.db.mongodb import convert_one from app.db.mongodb import MongoAlbums @@ -59,3 +60,12 @@ class Albums(MongoAlbums): """ album = self.collection.find_one({"hash": hash}) return convert_one(album) + + def set_album_colors(self, colors: List[str], album_id: str) -> None: + """ + Sets the colors for an album. + """ + self.collection.update_one( + {"_id": ObjectId(album_id)}, + {"$set": {"colors": colors}}, + ) diff --git a/server/app/functions.py b/server/app/functions.py index e62128a9..4bccdd64 100644 --- a/server/app/functions.py +++ b/server/app/functions.py @@ -15,6 +15,7 @@ from app.lib.albumslib import ValidateAlbumThumbs from app.lib import trackslib from app.lib.populate import CreateAlbums, Populate from app.lib.playlistlib import ValidatePlaylistThumbs +from app.lib.colorlib import ProcessAlbumColors @helpers.background @@ -23,17 +24,20 @@ def run_checks(): Checks for new songs every 5 minutes. """ - # while True: - trackslib.validate_tracks() + while True: + trackslib.validate_tracks() - Populate() - CreateAlbums() + Populate() + CreateAlbums() - if helpers.Ping()(): - CheckArtistImages()() + if helpers.Ping()(): + CheckArtistImages()() - ValidateAlbumThumbs() - ValidatePlaylistThumbs() + ValidateAlbumThumbs() + ValidatePlaylistThumbs() + ProcessAlbumColors() + + time.sleep(300) @helpers.background diff --git a/server/app/lib/colorlib.py b/server/app/lib/colorlib.py index 5882461f..e410d870 100644 --- a/server/app/lib/colorlib.py +++ b/server/app/lib/colorlib.py @@ -1,17 +1,19 @@ -from io import BytesIO - import colorgram -from app import api from app import instances -from app.lib.taglib import return_album_art -from PIL import Image -from progress.bar import Bar -def get_image_colors(image) -> list: +from app.helpers import Get +from app import settings +from app.models import Album +from app.logger import get_logger + +log = get_logger() + + +def get_image_colors(image: str) -> list: """Extracts 2 of the most dominant colors from an image.""" try: - colors = sorted(colorgram.extract(image, 2), key=lambda c: c.hsl.h) + colors = sorted(colorgram.extract(image, 4), key=lambda c: c.hsl.h) except OSError: return [] @@ -24,30 +26,25 @@ def get_image_colors(image) -> list: return formatted_colors -def save_track_colors(img, filepath) -> None: - """Saves the track colors to the database""" +class ProcessAlbumColors: + def __init__(self) -> None: + log.info("Processing album colors") + all_albums = Get.get_all_albums() - track_colors = get_image_colors(img) + all_albums = [a for a in all_albums if len(a.colors) == 0] - tc_dict = { - "filepath": filepath, - "colors": track_colors, - } + for a in all_albums: + self.process_color(a) - instances.track_color_instance.insert_track_color(tc_dict) + log.info("Processing album colors ... ✅") + @staticmethod + def process_color(album: Album): + img = settings.THUMBS_PATH + "/" + album.image -def save_t_colors(): - _bar = Bar("Processing image colors", max=len(api.DB_TRACKS)) + colors = get_image_colors(img) - for track in api.DB_TRACKS: - filepath = track["filepath"] - album_art = return_album_art(filepath) + if len(colors) > 0: + instances.album_instance.set_album_colors(colors, album.albumid) - if album_art is not None: - img = Image.open(BytesIO(album_art)) - save_track_colors(img, filepath) - - _bar.next() - - _bar.finish() + return colors diff --git a/server/app/models.py b/server/app/models.py index 915e225c..3cb4d42b 100644 --- a/server/app/models.py +++ b/server/app/models.py @@ -82,6 +82,7 @@ class Album: Creates an album object """ + albumid: str title: str artist: str hash: str @@ -92,14 +93,21 @@ class Album: is_soundtrack: bool = False is_compilation: bool = False is_single: bool = False + colors: List[str] = field(default_factory=list) def __init__(self, tags): + self.albumid = tags["_id"]["$oid"] self.title = tags["title"] self.artist = tags["artist"] self.date = tags["date"] self.image = tags["image"] self.hash = tags["hash"] + try: + self.colors = tags["colors"] + except KeyError: + self.colors = [] + @property def is_soundtrack(self) -> bool: keywords = ["motion picture", "soundtrack"] diff --git a/src/components/AlbumView/Header.vue b/src/components/AlbumView/Header.vue index 5c41c3e0..7a016d2d 100644 --- a/src/components/AlbumView/Header.vue +++ b/src/components/AlbumView/Header.vue @@ -1,6 +1,13 @@ From 5acb8cb84d3c94e5e599fa643af32740628588c9 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Thu, 30 Jun 2022 17:35:46 +0300 Subject: [PATCH 05/22] check if album colors have contrast - remove albumid field from album class - set accent color to $red --- server/app/api/album.py | 12 +++++- server/app/db/mongodb/albums.py | 4 +- server/app/lib/colorlib.py | 2 +- server/app/models.py | 2 - src/assets/css/_variables.scss | 2 +- src/components/AlbumView/Header.vue | 50 +++++++++++------------ src/components/LeftSidebar/Navigation.vue | 2 +- src/components/shared/PlayBtnRect.vue | 5 ++- src/composables/pages/album.ts | 1 + 9 files changed, 42 insertions(+), 38 deletions(-) diff --git a/server/app/api/album.py b/server/app/api/album.py index 64ed1da3..f4ec080e 100644 --- a/server/app/api/album.py +++ b/server/app/api/album.py @@ -41,20 +41,28 @@ def get_album(): """Returns all the tracks in the given album.""" data = request.get_json() albumhash = data["hash"] + error_msg = {"error": "Album not created yet."} tracks = instances.tracks_instance.find_tracks_by_hash(albumhash) + + if len(tracks) == 0: + return error_msg, 204 + tracks = [models.Track(t) for t in tracks] tracks = helpers.RemoveDuplicates(tracks)() album = instances.album_instance.find_album_by_hash(albumhash) if not album: - return {"error": "Album not created yet."}, 204 + return error_msg, 204 album = models.Album(album) album.count = len(tracks) - album.duration = albumslib.get_album_duration(tracks) + try: + album.duration = albumslib.get_album_duration(tracks) + except AttributeError: + album.duration = 0 if ( album.count == 1 diff --git a/server/app/db/mongodb/albums.py b/server/app/db/mongodb/albums.py index 4999c146..eeceb6d0 100644 --- a/server/app/db/mongodb/albums.py +++ b/server/app/db/mongodb/albums.py @@ -61,11 +61,11 @@ class Albums(MongoAlbums): album = self.collection.find_one({"hash": hash}) return convert_one(album) - def set_album_colors(self, colors: List[str], album_id: str) -> None: + def set_album_colors(self, colors: List[str], hash: str) -> None: """ Sets the colors for an album. """ self.collection.update_one( - {"_id": ObjectId(album_id)}, + {"hash": hash}, {"$set": {"colors": colors}}, ) diff --git a/server/app/lib/colorlib.py b/server/app/lib/colorlib.py index e410d870..38f8e481 100644 --- a/server/app/lib/colorlib.py +++ b/server/app/lib/colorlib.py @@ -45,6 +45,6 @@ class ProcessAlbumColors: colors = get_image_colors(img) if len(colors) > 0: - instances.album_instance.set_album_colors(colors, album.albumid) + instances.album_instance.set_album_colors(colors, album.hash) return colors diff --git a/server/app/models.py b/server/app/models.py index 3cb4d42b..fc24ed12 100644 --- a/server/app/models.py +++ b/server/app/models.py @@ -82,7 +82,6 @@ class Album: Creates an album object """ - albumid: str title: str artist: str hash: str @@ -96,7 +95,6 @@ class Album: colors: List[str] = field(default_factory=list) def __init__(self, tags): - self.albumid = tags["_id"]["$oid"] self.title = tags["title"] self.artist = tags["artist"] self.date = tags["date"] diff --git a/src/assets/css/_variables.scss b/src/assets/css/_variables.scss index 52fe2039..caad1304 100644 --- a/src/assets/css/_variables.scss +++ b/src/assets/css/_variables.scss @@ -39,7 +39,7 @@ $teal: rgb(64, 200, 224); $primary: $gray4; -$accent: $darkblue; +$accent: $red; $secondary: $gray5; $cta: $blue; $danger: $red; diff --git a/src/components/AlbumView/Header.vue b/src/components/AlbumView/Header.vue index 7a016d2d..c5b7ea58 100644 --- a/src/components/AlbumView/Header.vue +++ b/src/components/AlbumView/Header.vue @@ -4,7 +4,7 @@ class="a-header rounded" :style="{ backgroundImage: `linear-gradient( - 37deg, ${album.colors[0]}, ${album.colors[3]} + 37deg, ${props.album.colors[0]}, ${props.album.colors[3]} )`, }" > @@ -17,7 +17,7 @@ v-motion-slide-from-left >
-
+
Soundtrack @@ -46,14 +46,14 @@ import useVisibility from "@/composables/useVisibility"; import useNavStore from "@/stores/nav"; import useAlbumStore from "@/stores/pages/album"; -import { ref } from "vue"; +import { reactive, ref } from "vue"; import { playSources } from "../../composables/enums"; import { formatSeconds } from "../../composables/perks"; import { paths } from "../../config"; import { AlbumInfo } from "../../interfaces"; import PlayBtnRect from "../shared/PlayBtnRect.vue"; -defineProps<{ +const props = defineProps<{ album: AlbumInfo; }>(); @@ -61,7 +61,22 @@ const albumheaderthing = ref(null); const imguri = paths.images.thumb; const nav = useNavStore(); +const colors = reactive({ + color1: props.album.colors[0], + color2: props.album.colors[3], +}); + useVisibility(albumheaderthing, nav.toggleShowPlay); + +function isLight(rgb: string = props.album.colors[0]) { + if (rgb == null || undefined) return false; + + const [r, g, b] = rgb.match(/\d+/g)!.map(Number); + const brightness = (r * 299 + g * 587 + b * 114) / 1000; + console.log(brightness); + + return brightness > 150; +} diff --git a/src/components/shared/PlayBtnRect.vue b/src/components/shared/PlayBtnRect.vue index 0d5f1b4f..0f9f6000 100644 --- a/src/components/shared/PlayBtnRect.vue +++ b/src/components/shared/PlayBtnRect.vue @@ -11,7 +11,7 @@ diff --git a/src/components/shared/SongItem.vue b/src/components/shared/SongItem.vue index d7b8bc3b..d904db8c 100644 --- a/src/components/shared/SongItem.vue +++ b/src/components/shared/SongItem.vue @@ -157,6 +157,10 @@ function emitUpdate(track: Track) { max-width: max-content; cursor: pointer; + &:hover { + text-decoration: underline; + } + @include tablet-portrait { display: none; } From 77a5d2b7c24c0f17409ba3a503b5b22d8b4b5c9e Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Sat, 2 Jul 2022 08:21:10 +0300 Subject: [PATCH 07/22] send processing album colors to a background thread - use white color as default album page play button color - return 404 if album is None on get_album_bio() --- server/app/api/album.py | 7 +++- server/app/functions.py | 23 ++++++++---- server/app/lib/populate.py | 2 +- src/components/AlbumView/Header.vue | 13 ++----- .../PlaylistView/FeaturedArtists.vue | 37 +++++-------------- src/components/contextMenu.vue | 4 +- src/views/AlbumView.vue | 26 ++++--------- 7 files changed, 44 insertions(+), 68 deletions(-) diff --git a/server/app/api/album.py b/server/app/api/album.py index f4ec080e..1a616a70 100644 --- a/server/app/api/album.py +++ b/server/app/api/album.py @@ -80,12 +80,17 @@ def get_album_bio(): """Returns the album bio for the given album.""" data = request.get_json() album_hash = data["hash"] + err_msg = {"bio": "No bio found"} album = instances.album_instance.find_album_by_hash(album_hash) + + if album is None: + return err_msg, 404 + bio = FetchAlbumBio(album["title"], album["artist"])() if bio is None: - return {"bio": "No bio found."}, 404 + return err_msg, 404 return {"bio": bio} diff --git a/server/app/functions.py b/server/app/functions.py index 4bccdd64..3e8ab7e8 100644 --- a/server/app/functions.py +++ b/server/app/functions.py @@ -16,13 +16,16 @@ from app.lib import trackslib from app.lib.populate import CreateAlbums, Populate from app.lib.playlistlib import ValidatePlaylistThumbs from app.lib.colorlib import ProcessAlbumColors +from app.logger import get_logger +log = get_logger() @helpers.background def run_checks(): """ Checks for new songs every 5 minutes. """ + ValidateAlbumThumbs() while True: trackslib.validate_tracks() @@ -33,9 +36,12 @@ def run_checks(): if helpers.Ping()(): CheckArtistImages()() - ValidateAlbumThumbs() + @helpers.background + def process_album_colors(): + ProcessAlbumColors() + ValidatePlaylistThumbs() - ProcessAlbumColors() + process_album_colors() time.sleep(300) @@ -80,14 +86,17 @@ class useImageDownloader: img = Image.open(BytesIO(requests.get(self.url).content)) img.save(self.dest, format="webp") img.close() + return "fetched image" except requests.exceptions.ConnectionError: time.sleep(5) + return "connection error" class CheckArtistImages: def __init__(self): self.artists: list[str] = [] print("Checking for artist images") + log.info("Checking artist images") @staticmethod def check_if_exists(img_path: str): @@ -116,21 +125,21 @@ class CheckArtistImages: ) if cls.check_if_exists(img_path): - return + return "exists" url = getArtistImage(artistname)() if url is None: - return - useImageDownloader(url, img_path)() + return "url is none" + + return useImageDownloader(url, img_path)() def __call__(self): self.artists = helpers.Get.get_all_artists() with ThreadPoolExecutor() as pool: iter = pool.map(self.download_image, self.artists) - for i in iter: - pass + [print(i) for i in iter] print("Done fetching images") diff --git a/server/app/lib/populate.py b/server/app/lib/populate.py index 417e5e7a..81c8774f 100644 --- a/server/app/lib/populate.py +++ b/server/app/lib/populate.py @@ -144,7 +144,7 @@ class CreateAlbums: album = create_album(track) self.db_tracks.remove(track) else: - album["image"] = hash + album["image"] = hash + ".webp" try: album = Album(album) return album diff --git a/src/components/AlbumView/Header.vue b/src/components/AlbumView/Header.vue index 270a4899..91975789 100644 --- a/src/components/AlbumView/Header.vue +++ b/src/components/AlbumView/Header.vue @@ -78,16 +78,14 @@ function isLight(rgb: string = props.album.colors[0]) { const [r, g, b] = rgb.match(/\d+/g)!.map(Number); const brightness = (r * 299 + g * 587 + b * 114) / 1000; - return brightness > 170; + return brightness > 150; } function getButtonColor(colors: string[] = props.album.colors) { const base_color = colors[0]; - console.log(colors.length); - if (colors.length === 0) return { color: "#000" }; + if (colors.length === 0) return { color: "#fff", isDark: true }; for (let i = 0; i < colors.length; i++) { - // if (isLight(colors[i])) break; if (theyContrast(base_color, colors[i])) { return { color: colors[i], @@ -97,7 +95,8 @@ function getButtonColor(colors: string[] = props.album.colors) { } return { - color: "#000", + color: "#fff", + isDark: true, }; } @@ -124,10 +123,6 @@ function rgbToArray(rgb: string) { function theyContrast(color1: string, color2: string) { return contrast(rgbToArray(color1), rgbToArray(color2)) > 3; } - -console.log( - contrast(rgbToArray(props.album.colors[0]), rgbToArray(props.album.colors[3])) -); From 14182e78cda99ac90a960bdec80986cffe9862fd Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Sat, 2 Jul 2022 11:02:29 +0300 Subject: [PATCH 11/22] add a bottom padding when the bottom area is expanded - attach a ''resetBottomPadding" event to the album header component - add function documentation to the header and albumview components. --- src/components/AlbumView/Header.vue | 73 ++++++++++++++++++++++++----- src/components/shared/SongItem.vue | 1 - src/views/AlbumView.vue | 31 +++++++++--- 3 files changed, 87 insertions(+), 18 deletions(-) diff --git a/src/components/AlbumView/Header.vue b/src/components/AlbumView/Header.vue index 30bc63c5..26a979d9 100644 --- a/src/components/AlbumView/Header.vue +++ b/src/components/AlbumView/Header.vue @@ -61,18 +61,37 @@ const props = defineProps<{ album: AlbumInfo; }>(); +const emit = defineEmits<{ + (event: "resetBottomPadding"): void; +}>(); + const albumheaderthing = ref(null); const imguri = paths.images.thumb; const nav = useNavStore(); -const colors = reactive({ - color1: props.album.colors[0], - color2: props.album.colors[3], -}); +/** + * Calls the `toggleShowPlay` method which toggles the play button in the nav. + * Emits the `resetBottomPadding` event to reset the album page content bottom padding. + * + * @param {boolean} state the new visibility state of the album page header. + */ +function handleVisibilityState(state: boolean) { + if (state) { + emit("resetBottomPadding"); + } -useVisibility(albumheaderthing, nav.toggleShowPlay); + nav.toggleShowPlay(state); +} -function isLight(rgb: string = props.album.colors[0]) { +useVisibility(albumheaderthing, handleVisibilityState); + +/** + * Returns `true` if the rgb color passed is light. + * + * @param {string} rgb The color to check whether it's light or dark. + * @returns {boolean} true if color is light, false if color is dark. + */ +function isLight(rgb: string = props.album.colors[0]): boolean { if (rgb == null || undefined) return false; const [r, g, b] = rgb.match(/\d+/g)!.map(Number); @@ -81,7 +100,18 @@ function isLight(rgb: string = props.album.colors[0]) { return brightness > 170; } -function getButtonColor(colors: string[] = props.album.colors) { +interface BtnColor { + color: string; + isDark: boolean; +} + +/** + * Returns the first contrasting color in the album colors. + * + * @param {string[]} colors The album colors to choose from. + * @returns {BtnColor} A color to use as the play button background + */ +function getButtonColor(colors: string[] = props.album.colors): BtnColor { const base_color = colors[0]; if (colors.length === 0) return { color: "#fff", isDark: true }; @@ -100,6 +130,12 @@ function getButtonColor(colors: string[] = props.album.colors) { }; } +/** + * Returns the luminance of a color. + * @param r The red value of the color. + * @param g The green value of the color. + * @param b The blue value of the color. + */ function luminance(r: any, g: any, b: any) { let a = [r, g, b].map(function (v) { v /= 255; @@ -108,18 +144,33 @@ function luminance(r: any, g: any, b: any) { return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722; } -function contrast(rgb1: number[], rgb2: number[]) { - let lum1 = luminance(rgb1[0], rgb1[1], rgb1[2]); - let lum2 = luminance(rgb2[0], rgb2[1], rgb2[2]); +/** + * Returns a contrast ratio of `color1`:`color2` + * @param {string} color1 The first color + * @param {string} color2 The second color + */ +function contrast(color1: number[], color2: number[]): number { + let lum1 = luminance(color1[0], color1[1], color1[2]); + let lum2 = luminance(color2[0], color2[1], color2[2]); let brightest = Math.max(lum1, lum2); let darkest = Math.min(lum1, lum2); return (brightest + 0.05) / (darkest + 0.05); } -function rgbToArray(rgb: string) { +/** + * Converts a rgb color string to an array of the form: `[r, g, b]` + * @param rgb The color to convert + * @returns {number[]} The array representation of the color + */ +function rgbToArray(rgb: string): number[] { return rgb.match(/\d+/g)!.map(Number); } +/** + * Returns true if the `color2` contrast with `color1`. + * @param color1 The first color + * @param color2 The second color + */ function theyContrast(color1: string, color2: string) { return contrast(rgbToArray(color1), rgbToArray(color2)) > 3; } diff --git a/src/components/shared/SongItem.vue b/src/components/shared/SongItem.vue index d904db8c..8d2d63a7 100644 --- a/src/components/shared/SongItem.vue +++ b/src/components/shared/SongItem.vue @@ -127,7 +127,6 @@ function emitUpdate(track: Track) { text-align: left; gap: $small; user-select: none; - -moz-user-select: none; @include tablet-landscape { grid-template-columns: 1.5rem 1.5fr 1fr 1fr 2.5rem; diff --git a/src/views/AlbumView.vue b/src/views/AlbumView.vue index 157bcc4e..d21ffdd0 100644 --- a/src/views/AlbumView.vue +++ b/src/views/AlbumView.vue @@ -1,8 +1,8 @@