diff --git a/server/app/api.py b/server/app/api.py index 1884da9..9f69aea 100644 --- a/server/app/api.py +++ b/server/app/api.py @@ -1,9 +1,11 @@ +from crypt import methods import os from pprint import pprint import urllib from typing import List from flask import Blueprint, request, send_file -from app import functions, instances, helpers, cache + +from app import functions, instances, helpers, cache, models bp = Blueprint("api", __name__, url_prefix="") @@ -30,21 +32,21 @@ def say_hi(): return "^ _ ^" -def get_tracks(query: str) -> List: +def get_tracks(query: str) -> List[models.Track]: """ Gets all songs with a given title. """ return [track for track in all_the_f_music if query.lower() in track.title.lower()] -def get_search_albums(query: str) -> List: +def get_search_albums(query: str) -> List[models.Track]: """ Gets all songs with a given album. """ return [track for track in all_the_f_music if query.lower() in track.album.lower()] -def get_artists(artist: str) -> List: +def get_artists(artist: str) -> List[models.Track]: """ Gets all songs with a given artist. """ @@ -151,12 +153,13 @@ def find_tracks(): return "🎸" -@bp.route("/album///artists") -@cache.cached() -def get_albumartists(album, artist): +@bp.route("/album/artists", methods=["POST"]) +def get_albumartists(): """Returns a list of artists featured in a given album.""" - album = album.replace("|", "/") - artist = artist.replace("|", "/") + data = request.get_json() + + album = data["album"] + artist = data["artist"] tracks = [] @@ -292,20 +295,24 @@ def get_albums(): return {"albums": albums} -@bp.route("/album//<artist>/tracks") -@cache.cached() -def get_album_tracks(title: str, artist: str): +@bp.route("/album/tracks", methods=["POST"]) +def get_album_tracks(): """Returns all the tracks in the given album.""" + data = request.get_json() + + album = data["album"] + artist = data["artist"] + songs = [] for track in all_the_f_music: - if track.albumartist == artist and track.album == title: + if track.albumartist == artist and track.album == album: songs.append(track) songs = helpers.remove_duplicates(songs) album_obj = { - "name": title, + "name": album, "count": len(songs), "duration": "56 Minutes", "image": songs[0].image, diff --git a/server/app/helpers.py b/server/app/helpers.py index 0b8d3df..e2786d1 100644 --- a/server/app/helpers.py +++ b/server/app/helpers.py @@ -7,6 +7,7 @@ import threading import time from typing import List import requests +import colorgram from io import BytesIO @@ -15,9 +16,10 @@ from PIL import Image from app import instances from app import functions from app import watchdoge +from app import models -home_dir = os.path.expanduser('~') + '/' -app_dir = os.path.join(home_dir, '.musicx') +home_dir = os.path.expanduser("~") + "/" +app_dir = os.path.join(home_dir, ".musicx") LAST_FM_API_KEY = "762db7a44a9e6fb5585661f5f2bdf23a" @@ -63,7 +65,7 @@ def run_fast_scandir(_dir: str, ext: list): files = [] for f in os.scandir(_dir): - if f.is_dir() and not f.name.startswith('.'): + if f.is_dir() and not f.name.startswith("."): subfolders.append(f.path) if f.is_file(): if os.path.splitext(f.name)[1].lower() in ext: @@ -77,24 +79,26 @@ def run_fast_scandir(_dir: str, ext: list): return subfolders, files -def remove_duplicates(array: list) -> list: +def remove_duplicates(tracklist: List[models.Track]) -> List[models.Track]: """ Removes duplicates from a list. Returns a list without duplicates. """ song_num = 0 - while song_num < len(array) - 1: - for index, song in enumerate(array): - if array[song_num].title == song.title and \ - array[song_num].album == song.album and \ - array[song_num].artists == song.artists and \ - index != song_num: - array.remove(song) + while song_num < len(tracklist) - 1: + for index, song in enumerate(tracklist): + if ( + tracklist[song_num].title == song.title + and tracklist[song_num].album == song.album + and tracklist[song_num].artists == song.artists + and index != song_num + ): + tracklist.remove(song) song_num += 1 - return array + return tracklist def save_image(url: str, path: str) -> None: @@ -104,7 +108,7 @@ def save_image(url: str, path: str) -> None: response = requests.get(url) img = Image.open(BytesIO(response.content)) - img.save(path, 'JPEG') + img.save(path, "JPEG") def is_valid_file(filename: str) -> bool: @@ -112,7 +116,7 @@ def is_valid_file(filename: str) -> bool: Checks if a file is valid. Returns True if it is, False if it isn't. """ - if filename.endswith('.flac') or filename.endswith('.mp3'): + if filename.endswith(".flac") or filename.endswith(".mp3"): return True else: return False @@ -123,11 +127,10 @@ def create_config_dir() -> None: Creates the config directory if it doesn't exist. """ - _home_dir = os.path.expanduser('~') + _home_dir = os.path.expanduser("~") config_folder = os.path.join(_home_dir, app_dir) - dirs = ["", "images", "images/defaults", - "images/artists", "images/thumbnails"] + dirs = ["", "images", "images/defaults", "images/artists", "images/thumbnails"] for _dir in dirs: path = os.path.join(config_folder, _dir) @@ -140,19 +143,32 @@ def create_config_dir() -> None: os.chmod(path, 0o755) -def get_all_songs() -> List: +def get_all_songs() -> List[models.Track]: """ Gets all songs under the ~/ directory. """ print("Getting all songs...") - tracks = [] + + tracks: list[models.Track] = [] for track in instances.songs_instance.get_all_songs(): try: os.chmod(os.path.join(track["filepath"]), 0o755) except FileNotFoundError: - instances.songs_instance.remove_song_by_filepath(track['filepath']) + instances.songs_instance.remove_song_by_filepath(track["filepath"]) tracks.append(functions.create_track_class(track)) return tracks + + +def extract_colors(image) -> list: + colors = sorted(colorgram.extract(image, 2), key=lambda c: c.hsl.h) + + formatted_colors = [] + + for color in colors: + color = f"rgb({color.rgb.r}, {color.rgb.g}, {color.rgb.b})" + formatted_colors.append(color) + + return formatted_colors diff --git a/server/poetry.lock b/server/poetry.lock index d46d0e8..22e5649 100644 --- a/server/poetry.lock +++ b/server/poetry.lock @@ -36,6 +36,17 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "colorgram.py" +version = "1.2.0" +description = "A Python module for extracting colors from images. Get a palette of any picture!" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pillow = ">=3.3.1" + [[package]] name = "flask" version = "2.0.2" @@ -234,7 +245,7 @@ watchdog = ["watchdog"] [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "43a8f1b3d32df323e4836559445b061c5ef7540471f75ffb3365b683e953f760" +content-hash = "c5fb66888aa3ddc0828c3c7794409039ada460ab96b3f824fd76caa85e27fbfb" [metadata.files] certifi = [ @@ -253,6 +264,10 @@ colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] +"colorgram.py" = [ + {file = "colorgram.py-1.2.0-py2.py3-none-any.whl", hash = "sha256:e990769fa6df7261a450c7d5bef3a1a062f09ba1214bff67b4d6f02970a1a27b"}, + {file = "colorgram.py-1.2.0.tar.gz", hash = "sha256:e77766a5f9de7207bdef8f1c22a702cbf09630eae3bc46a450b9d9f12a7bfdbf"}, +] flask = [ {file = "Flask-2.0.2-py3-none-any.whl", hash = "sha256:cb90f62f1d8e4dc4621f52106613488b5ba826b2e1e10a33eac92f723093ab6a"}, {file = "Flask-2.0.2.tar.gz", hash = "sha256:7b2fb8e934ddd50731893bdcdb00fc8c0315916f9fcd50d22c7cc1a95ab634e2"}, diff --git a/server/pyproject.toml b/server/pyproject.toml index 8f4c65e..363a0e9 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -16,6 +16,7 @@ progress = "^1.6" gunicorn = "^20.1.0" Pillow = "^9.0.1" Flask-Caching = "^1.10.1" +"colorgram.py" = "^1.2.0" [tool.poetry.dev-dependencies] diff --git a/src/assets/css/BottomBar/BottomBar.scss b/src/assets/css/BottomBar/BottomBar.scss index 30a7167..2e982d1 100644 --- a/src/assets/css/BottomBar/BottomBar.scss +++ b/src/assets/css/BottomBar/BottomBar.scss @@ -1,6 +1,6 @@ .b-bar { height: 100%; - border-top: solid 1px $gray; + background-color: $gray; .grid { display: grid; @@ -36,7 +36,7 @@ .artists { font-size: 0.8rem; - color: $red; + color: $white; } } } @@ -48,7 +48,6 @@ align-items: center; margin: $small; padding: $small; - background-color: $gray5; .progress-bottom { display: flex; diff --git a/src/components/AlbumView/Header.vue b/src/components/AlbumView/Header.vue index 7378e87..bc13ebf 100644 --- a/src/components/AlbumView/Header.vue +++ b/src/components/AlbumView/Header.vue @@ -1,18 +1,22 @@ <template> <div class="album-h"> <div class="a-header rounded"> + <div + class="image art shadow-lg" + :style="{ backgroundImage: `url("${encodeURI(props.album_info.image)}")` }" + ></div> <div class="info"> <div class="top"> <div class="h">Album</div> <div class="separator no-border"></div> - <div class="title">{{ album_info.name }}</div> - <div class="artist">{{ album_info.artist }}</div> + <div class="title">{{ props.album_info.name }}</div> + <div class="artist">{{ props.album_info.artist }}</div> </div> <div class="separator no-border"></div> <div class="bottom"> <div class="stats shadow-sm"> - {{ album_info.count }} Tracks • {{ album_info.duration }} • - {{ album_info.date }} + {{ props.album_info.count }} Tracks • {{ props.album_info.duration }} • + {{ props.album_info.date }} </div> <div class="play rounded" @click="playAlbum"> <div class="icon"></div> @@ -24,22 +28,20 @@ </div> </template> -<script> +<script setup> import state from "@/composables/state.js"; import perks from "@/composables/perks.js"; -export default { - props: ["album_info"], - setup() { - function playAlbum() { - perks.updateQueue(state.album_song_list.value[0], "album"); - } - - return { - playAlbum, - }; +const props = defineProps({ + album_info: { + type: Object, + default: () => ({}), }, -}; +}); + +function playAlbum() { + perks.updateQueue(state.album.tracklist[0], "album"); +} </script> <style lang="scss"> @@ -59,7 +61,7 @@ export default { overflow: hidden; display: flex; align-items: center; - padding: $small; + padding: 1rem; height: 100%; background-image: linear-gradient( 56deg, @@ -73,12 +75,20 @@ export default { background-repeat: no-repeat; background-size: cover; + .art { + position: absolute; + width: 12rem; + height: 12rem; + left: 1rem; + } + .info { width: 100%; height: calc(100%); display: flex; flex-direction: column; justify-content: flex-end; + margin-left: 13rem; .top { .h { @@ -88,6 +98,7 @@ export default { font-size: 2rem; font-weight: 1000; color: white; + text-transform: capitalize; } .artist { diff --git a/src/components/FolderView/Header.vue b/src/components/FolderView/Header.vue index 7c94947..d9a452c 100644 --- a/src/components/FolderView/Header.vue +++ b/src/components/FolderView/Header.vue @@ -13,6 +13,9 @@ </div> </div> <div class="search"> + <div class="loaderr"> + <Loader /> + </div> <input type="text" class="search-input border" @@ -26,11 +29,13 @@ <script> import perks from "@/composables/perks.js"; -import { watch } from '@vue/runtime-core'; -import useDebouncedRef from '@/composables/useDebouncedRef.js'; +import { watch } from "@vue/runtime-core"; +import useDebouncedRef from "@/composables/useDebouncedRef.js"; +import Loader from "../shared/Loader.vue"; export default { props: ["path", "first_song"], + components: { Loader }, setup(props, { emit }) { const query = useDebouncedRef("", 400); @@ -63,8 +68,13 @@ export default { .folder-top .search { width: 50%; display: grid; + grid-template-columns: 1fr 1fr; place-items: end; + .loaderr { + width: 2rem; + } + .search-input { max-width: 20rem; width: 100%; @@ -123,4 +133,4 @@ export default { } } } -</style> \ No newline at end of file +</style> diff --git a/src/components/RightSideBar/Search.vue b/src/components/RightSideBar/Search.vue index ab77e03..e4587e2 100644 --- a/src/components/RightSideBar/Search.vue +++ b/src/components/RightSideBar/Search.vue @@ -69,7 +69,7 @@ import useDebouncedRef from "@/composables/useDebouncedRef"; import AlbumGrid from "@/components/Search/AlbumGrid.vue"; import ArtistGrid from "@/components/Search/ArtistGrid.vue"; import TracksGrid from "@/components/Search/TracksGrid.vue"; -import Loader from "@/components/Search/Loader.vue"; +import Loader from "@/components/shared/Loader.vue"; import Options from "@/components/Search/Options.vue"; import Filters from "@/components/Search/Filters.vue"; import "@/assets/css/Search/Search.scss"; @@ -138,7 +138,7 @@ export default { } function loadMoreTracks(start) { - // scrollSearchThing(); + scrollSearchThing(); loadMore.loadMoreTracks(start).then((response) => { tracks.tracks = [...tracks.tracks, ...response.tracks]; tracks.more = response.more; diff --git a/src/components/Search/Loader.vue b/src/components/shared/Loader.vue similarity index 86% rename from src/components/Search/Loader.vue rename to src/components/shared/Loader.vue index 1fb5322..e6415c1 100644 --- a/src/components/Search/Loader.vue +++ b/src/components/shared/Loader.vue @@ -4,15 +4,10 @@ </div> </template> -<script> +<script setup> import state from "@/composables/state.js"; -export default { - setup() { - return { - loading: state.loading, - }; - }, -}; + +const loading = state.loading </script> <style lang="scss"> diff --git a/src/components/shared/SongItem.vue b/src/components/shared/SongItem.vue index dbfb20b..156d46d 100644 --- a/src/components/shared/SongItem.vue +++ b/src/components/shared/SongItem.vue @@ -1,10 +1,15 @@ <template> - <tr class="songlist-item" :class="{ current: current.trackid === song.trackid }" @dblclick="emitUpdate(song)"> + <tr + class="songlist-item" + :class="{ current: current.trackid === song.trackid }" + @dblclick="emitUpdate(song)" + > <td class="index">{{ index }}</td> - <td class="flex" @click="emitUpdate(song)"> + <td class="flex"> <div class="album-art image" :style="{ backgroundImage: `url("${song.image}"` }" + @click="emitUpdate(song)" > <div class="now-playing-track image" @@ -12,8 +17,8 @@ :class="{ active: is_playing, not_active: !is_playing }" ></div> </div> - <div> - <span class="ellip">{{ song.title }}</span> + <div @click="emitUpdate(song)"> + <span class="ellip title">{{ song.title }}</span> <div class="artist ellip"> <span v-for="artist in putCommas(song.artists)" :key="artist"> {{ artist }} @@ -35,7 +40,10 @@ </div> </td> <td class="song-album"> - <div class="ellip" @click="emitLoadAlbum(song.album, song.albumartist)"> + <div + class="album ellip" + @click="emitLoadAlbum(song.album, song.albumartist)" + > {{ song.album }} </div> </td> @@ -94,12 +102,10 @@ export default { } .song-duration { - font-size: .8rem; + font-size: 0.8rem; width: 5rem !important; } - cursor: pointer; - .flex { position: relative; padding-left: 4rem; @@ -113,13 +119,19 @@ export default { margin-right: 1rem; display: grid; place-items: center; - border-radius: .5rem; + border-radius: 0.5rem; + cursor: pointer; + } + + .title { + cursor: pointer; } .artist { display: none; font-size: 0.8rem; color: rgba(255, 255, 255, 0.719); + cursor: pointer; @include phone-only { display: unset; @@ -136,7 +148,7 @@ export default { border-radius: $small 0 0 $small; } - td:nth-child(2){ + td:nth-child(2) { border-radius: 0 $small $small 0; @include phone-only { @@ -194,12 +206,21 @@ export default { } .song-album { + .album { + cursor: pointer; + width: max-content; + } + @include tablet-portrait { display: none; } } .song-artists { + .artist { + cursor: pointer; + } + @include phone-only { display: none; } diff --git a/src/composables/album.js b/src/composables/album.js index ce2b8ab..4f1872b 100644 --- a/src/composables/album.js +++ b/src/composables/album.js @@ -1,44 +1,45 @@ -let base_uri = "http://0.0.0.0:9876"; +import axios from "axios"; +import state from "./state"; -const getAlbumTracks = async (name, artist) => { - const res = await fetch( - base_uri + - "/album/" + - encodeURIComponent(name) + "/" + - encodeURIComponent(artist) + - "/tracks" - ); +const getAlbumTracks = async (album, artist) => { + let data = {}; - if (!res.ok) { - const message = `An error has occurred: ${res.status}`; - throw new Error(message); - } + await axios + .post(state.settings.uri + "/album/tracks", { + album: album, + artist: artist, + }) + .then((res) => { + data = res.data; + }) + .catch((err) => { + console.error(err); + }); - return await res.json(); + return data; }; -const getAlbumArtists = async (name, artist) => { - const res = await fetch( - base_uri + - "/album/" + - encodeURIComponent(name.replaceAll("/", "|")) + - "/" + - encodeURIComponent(artist.replaceAll("/", "|")) + - "/artists" - ); +const getAlbumArtists = async (album, artist) => { + let artists = []; - if (!res.ok) { - const message = `An error has occurred: ${res.status}`; - throw new Error(message); - } + await axios + .post(state.settings.uri + "/album/artists", { + album: album, + artist: artist, + }) + .then((res) => { + artists = res.data.artists; + }) + .catch((err) => { + console.error(err); + }); - const data = await res.json(); - return data.artists; + return artists; }; const getAlbumBio = async (name, artist) => { const res = await fetch( - base_uri + + state.settings.uri + "/album/" + encodeURIComponent(name.replaceAll("/", "|")) + "/" + diff --git a/src/composables/perks.js b/src/composables/perks.js index e43fdd4..0b8c274 100644 --- a/src/composables/perks.js +++ b/src/composables/perks.js @@ -89,7 +89,7 @@ const updateQueue = async (song, type) => { list = state.folder_song_list.value; break; case "album": - list = state.album_song_list.value; + list = state.album.tracklist; break; } diff --git a/src/composables/routeLoader.js b/src/composables/routeLoader.js index dbdeb79..847fe5e 100644 --- a/src/composables/routeLoader.js +++ b/src/composables/routeLoader.js @@ -5,24 +5,25 @@ import state from "./state.js"; async function toAlbum(title, artist) { console.log("routing to album"); + state.loading.value = true; await album .getAlbumTracks(title, artist) .then((data) => { - state.album_song_list.value = data.songs; - state.album_info.value = data.info; + state.album.tracklist = data.songs; + state.album.info = data.info; }) .then( await album.getAlbumArtists(title, artist).then((data) => { - state.album_artists.value = data; + state.album.artists = data; }) ) .then( album.getAlbumBio(title, artist).then((data) => { if (data === "None") { - state.album_bio.value = null; + state.album.bio = null; } else { - state.album_bio.value = data; + state.album.bio = data; } }) ) diff --git a/src/composables/state.js b/src/composables/state.js index a5cfd3b..9393ab2 100644 --- a/src/composables/state.js +++ b/src/composables/state.js @@ -1,4 +1,5 @@ import { ref } from "@vue/reactivity"; +import { reactive } from "vue"; const search_query = ref(""); @@ -33,10 +34,12 @@ const prev = ref({ }, }); -const album_song_list = ref([]); -const album_info = ref([]); -const album_artists = ref([]); -const album_bio = ref(""); +const album = reactive({ + tracklist: [], + info: {}, + artists: [], + bio: "", +}); const filters = ref([]); @@ -45,9 +48,9 @@ const loading = ref(false); const is_playing = ref(false); -const search_tracks = ref([]); -const search_albums = ref([]); -const search_artists = ref([]); +const settings = reactive({ + uri: "http://0.0.0.0:9876", +}) export default { search_query, @@ -60,11 +63,6 @@ export default { magic_flag, loading, is_playing, - search_tracks, - search_albums, - search_artists, - album_song_list, - album_info, - album_artists, - album_bio, + album, + settings, }; diff --git a/src/main.js b/src/main.js index 89776f2..6daa2e9 100644 --- a/src/main.js +++ b/src/main.js @@ -5,11 +5,6 @@ import router from "./router"; import "../src/assets/css/global.scss"; -import mitt from "mitt"; - -const emitter = mitt(); - const app = createApp(App); app.use(router); -app.provide('emitter', emitter); app.mount('#app'); \ No newline at end of file diff --git a/src/views/AlbumView.vue b/src/views/AlbumView.vue index 4129682..2a701f3 100644 --- a/src/views/AlbumView.vue +++ b/src/views/AlbumView.vue @@ -1,26 +1,24 @@ <template> <div class="al-view rounded"> <div> - <Header :album_info="album_info" /> + <Header :album_info="state.album.info" /> </div> <div class="separator" id="av-sep"></div> <div class="songs rounded"> - <SongList :songs="album_songs" /> + <SongList :songs="state.album.tracklist" /> </div> <div class="separator" id="av-sep"></div> - <FeaturedArtists :artists="artists" /> - <div v-if="bio"> + <FeaturedArtists :artists="state.album.artists" /> + <div v-if="state.album.bio"> <div class="separator" id="av-sep"></div> - <AlbumBio :bio="bio" v-if="bio" /> + <AlbumBio :bio="state.album.bio" v-if="state.album.bio" /> </div> - <!-- <div class="separator" id="av-sep"></div> --> </div> </template> -<script> +<script setup> import { useRoute } from "vue-router"; import { onMounted } from "@vue/runtime-core"; -import { onUnmounted } from "@vue/runtime-core"; import { watch } from "vue"; import Header from "../components/AlbumView/Header.vue"; import AlbumBio from "../components/AlbumView/AlbumBio.vue"; @@ -31,45 +29,20 @@ import FeaturedArtists from "../components/PlaylistView/FeaturedArtists.vue"; import state from "@/composables/state.js"; import routeLoader from "@/composables/routeLoader.js"; -export default { - components: { - Header, - AlbumBio, - SongList, - FeaturedArtists, - }, - setup() { - const route = useRoute(); +const route = useRoute(); - watch( - () => route.params, - () => { - if (route.name === "AlbumView") { - routeLoader.toAlbum(route.params.album, route.params.artist); - } +onMounted(() => { + routeLoader.toAlbum(route.params.album, route.params.artist); + + watch( + () => route.params, + () => { + if (route.name === "AlbumView") { + routeLoader.toAlbum(route.params.album, route.params.artist); } - ); - - onMounted(() => { - console.log("mounted"); - routeLoader.toAlbum(route.params.album, route.params.artist); - }); - - onUnmounted(() => { - state.album_song_list.value = []; - state.album_info.value = {}; - state.album_artists.value = []; - state.album_bio.value = ""; - }); - - return { - album_songs: state.album_song_list, - album_info: state.album_info, - artists: state.album_artists, - bio: state.album_bio, - }; - }, -}; + } + ); +}); </script> <style lang="scss"> diff --git a/src/views/FolderView.vue b/src/views/FolderView.vue index abad4e3..a3f8069 100644 --- a/src/views/FolderView.vue +++ b/src/views/FolderView.vue @@ -101,13 +101,16 @@ export default { getDirData(path.value); - watch(route, (new_route) => { - path.value = new_route.params.path; + watch( + () => route.params, + () => { + path.value = route.params.path; - if (!path.value) return; + if (!path.value) return; - getDirData(path.value); - }); + getDirData(path.value); + } + ); }); function updateQueryString(value) {