Migrate to vite and some more stuff

- server: add a `Track` class
- server: add a create_track_class function
- client: migrate from vue-cli to vite
This commit is contained in:
geoffrey45 2022-01-25 11:51:26 +03:00
parent 7689f13fdc
commit d6204946c2
18 changed files with 638 additions and 8372 deletions

View File

@ -1,17 +1,11 @@
module.exports = { module.exports = {
root: true, root: true,
env: { env: {
node: true es2021: true,
},
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended'
],
parserOptions: {
parser: 'babel-eslint'
}, },
extends: ["plugin:vue/vue3-essential", "eslint:recommended"],
rules: { rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', "no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
} },
} };

View File

@ -1,5 +0,0 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

21
index.html Normal file
View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="img/favicon.ico" />
<title>MusicX</title>
</head>
<body>
<noscript>
<strong
>We're sorry but this app doesn't work properly without JavaScript
enabled. Please enable it to continue.</strong
>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@ -3,16 +3,16 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "dev": "vite",
"build": "vue-cli-service build", "serve": "vite preview",
"lint": "vue-cli-service lint" "build": "vite build",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src"
}, },
"dependencies": { "dependencies": {
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"core-js": "^3.6.5",
"mitt": "^3.0.0", "mitt": "^3.0.0",
"register-service-worker": "^1.7.1", "register-service-worker": "^1.7.1",
"sass": "^1.44.0", "sass": "^1.49.0",
"sass-loader": "^10", "sass-loader": "^10",
"vue": "^3.0.0", "vue": "^3.0.0",
"vue-debounce": "^3.0.2", "vue-debounce": "^3.0.2",
@ -20,14 +20,10 @@
"webpack": "^5.64.4" "webpack": "^5.64.4"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0", "@vitejs/plugin-vue": "^1.6.1",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-pwa": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0", "@vue/compiler-sfc": "^3.0.0",
"babel-eslint": "^10.1.0", "eslint": "^8.7.0",
"eslint": "^6.7.2", "eslint-plugin-vue": "^8.3.0",
"eslint-plugin-vue": "^7.0.0" "vite": "^2.5.4"
} }
} }

View File

@ -1,17 +0,0 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="img/favicon.ico">
<title>MusicX</title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@ -11,8 +11,11 @@ all_the_f_music = helpers.getAllSongs()
def initialize() -> None: def initialize() -> None:
"""
Runs all the necessary setup functions.
"""
helpers.create_config_dir() helpers.create_config_dir()
helpers.check_for_new_songs() # helpers.check_for_new_songs()
initialize() initialize()
@ -38,19 +41,19 @@ def search_by_title():
artists_dicts = [] artists_dicts = []
for track in all_the_f_music: for track in all_the_f_music:
if query.lower() in track['title'].lower(): if query.lower() in track.title.lower():
tracks.append(track) tracks.append(track)
if query.lower() in track['album'].lower(): if query.lower() in track.album.lower():
albums.append(track) albums.append(track)
if query.lower() in str(track['artists']).lower(): if query.lower() in str(track.artists).lower():
artists.append(track) artists.append(track)
for song in albums: for song in albums:
album_obj = { album_obj = {
"name": song["album"], "name": song.album,
"artist": song["album_artist"], "artist": song.album_artist,
} }
if album_obj not in albums_dicts: if album_obj not in albums_dicts:
@ -58,11 +61,11 @@ def search_by_title():
for album in albums_dicts: for album in albums_dicts:
for track in albums: for track in albums:
if album['name'] == track['album']: if album['name'] == track.album:
album['image'] = track['image'] album['image'] = track.image
for song in artists: for song in artists:
for artist in song["artists"]: for artist in song.artists:
if query.lower() in artist.lower(): if query.lower() in artist.lower():
artist_obj = { artist_obj = {
@ -80,22 +83,23 @@ def search_by_title():
else: else:
more_tracks = False more_tracks = False
if len(artists_dicts) > 5: if len(artists_dicts) > 8:
more_artists = True more_artists = True
else: else:
more_artists = False more_artists = False
if len(albums_dicts) > 5: if len(albums_dicts) > 8:
more_albums = True more_albums = True
else: else:
more_albums = False more_albums = False
return {'data': [ return {'data': [
{'tracks': tracks[:5], 'more': more_tracks}, {'tracks': tracks[:5], 'more': more_tracks},
{'albums': albums_dicts[:5], 'more': more_albums}, {'albums': albums_dicts[:8], 'more': more_albums},
{'artists': artists_dicts[:5], 'more': more_artists} {'artists': artists_dicts[:8], 'more': more_artists}
]} ]}
@bp.route('/populate') @bp.route('/populate')
def x(): def x():
functions.populate() functions.populate()
@ -111,13 +115,13 @@ def get_album_artists(album, artist):
tracks = [] tracks = []
for track in all_the_f_music: for track in all_the_f_music:
if track["album"] == album and track["album_artist"] == artist: if track.album == album and track.album_artist == artist:
tracks.append(track) tracks.append(track)
artists = [] artists = []
for track in tracks: for track in tracks:
for artist in track['artists']: for artist in track.artists:
if artist not in artists: if artist not in artists:
artists.append(artist) artists.append(artist)
@ -213,35 +217,13 @@ def getFolderTree(folder: str):
songs = [] songs = []
for x in all_the_f_music: for track in all_the_f_music:
if x['folder'] == req_dir: if track.folder == req_dir:
songs.append(x) songs.append(track)
for song in songs:
song['image'] = song['image'].replace('127.0.0.1', '0.0.0.0')
return {"files": helpers.remove_duplicates(songs), "folders": sorted(folders, key=lambda i: i['name'])} return {"files": helpers.remove_duplicates(songs), "folders": sorted(folders, key=lambda i: i['name'])}
@bp.route('/qwerty')
def populateArtists():
all_songs = instances.songs_instance.get_all_songs()
artists = []
for song in all_songs:
for a in song['artists']:
a_obj = {
"name": a,
}
if a_obj not in artists:
artists.append(a_obj)
instances.artist_instance.insert_artist(a_obj)
return {'songs': artists}
@bp.route('/albums') @bp.route('/albums')
def getAlbums(): def getAlbums():
@ -310,8 +292,3 @@ def convert_images_to_webp():
'.jpg', '.webp')), format='webp') '.jpg', '.webp')), format='webp')
return "Done" return "Done"
@bp.route('/test')
def test_http_status_response():
return "OK", 200

View File

@ -6,6 +6,7 @@ import time
import os import os
import requests import requests
import random import random
import datetime
from mutagen.flac import MutagenError from mutagen.flac import MutagenError
from mutagen.mp3 import MP3 from mutagen.mp3 import MP3
@ -18,6 +19,7 @@ from io import BytesIO
from app import helpers from app import helpers
from app import instances from app import instances
from app import api from app import api
from app import models
def populate(): def populate():
@ -53,61 +55,63 @@ def populate_images():
bar = Bar('Processing images', max=len(artists)) bar = Bar('Processing images', max=len(artists))
for artist in artists: for artist in artists:
file_path = helpers.app_dir + '/images/artists/' + \ file_path = helpers.app_dir + '/images/artists/' + \
artist.replace('/', '::') + '.jpg' artist.replace('/', '::') + '.webp'
if not os.path.exists(file_path): if not os.path.exists(file_path):
def try_save_image():
url = 'https://api.deezer.com/search/artist?q={}'.format(artist) url = 'https://api.deezer.com/search/artist?q={}'.format(artist)
try:
response = requests.get(url) response = requests.get(url)
except requests.ConnectionError:
print('\n sleeping for 5 seconds')
time.sleep(5)
response = requests.get(url)
data = response.json() data = response.json()
try: try:
img_data = data['data'][0]['picture_xl'] img_path = data['data'][0]['picture_medium']
except: except:
img_data = None img_path = None
if img_data is not None: if img_path is not None:
helpers.save_image(img_data, file_path) # save image as webp
img = Image.open(BytesIO(requests.get(img_path).content))
img.save(file_path, format="webp")
try:
try_save_image()
except requests.exceptions.ConnectionError:
time.sleep(5)
try_save_image()
bar.next() bar.next()
bar.finish() bar.finish()
def extract_thumb(path: str) -> str: def extract_thumb(audio_file_path: str = None) -> str:
""" """
Extracts the thumbnail from an audio file. Returns the path to the thumbnail. Extracts the thumbnail from an audio file. Returns the path to the thumbnail.
""" """
album_art = None
def use_defaults() -> str: def use_defaults() -> str:
""" """
Returns a path to a random image in the defaults directory. Returns a path to a random image in the defaults directory.
""" """
path = "http://127.0.0.1:8900/images/defaults/" + \ path = str(random.randint(0, 10)) + '.webp'
str(random.randint(0, 10)) + '.webp'
return path return path
webp_path = path.split('/')[-1] + '.webp' webp_path = audio_file_path.split('/')[-1] + '.webp'
img_path = os.path.join(helpers.app_dir, "images", "thumbnails", webp_path) img_path = os.path.join(helpers.app_dir, "images", "thumbnails", webp_path)
if os.path.exists(img_path): if os.path.exists(img_path):
return "http://127.0.0.1:8900/images/thumbnails/" + webp_path return webp_path
if path.endswith('.flac'): if audio_file_path.endswith('.flac'):
try: try:
audio = FLAC(path) audio = FLAC(audio_file_path)
album_art = audio.pictures[0].data album_art = audio.pictures[0].data
except: except:
album_art = None album_art = None
elif path.endswith('.mp3'): elif audio_file_path.endswith('.mp3'):
try: try:
audio = ID3(path) audio = ID3(audio_file_path)
album_art = audio.getall('APIC')[0].data album_art = audio.getall('APIC')[0].data
except: except:
album_art = None album_art = None
@ -128,9 +132,8 @@ def extract_thumb(path: str) -> str:
except: except:
return use_defaults() return use_defaults()
final_path = "http://0.0.0.0:8900/images/thumbnails/" + webp_path return webp_path
return final_path
def getTags(full_path: str) -> dict: def getTags(full_path: str) -> dict:
""" """
@ -198,8 +201,23 @@ def getTags(full_path: str) -> dict:
except IndexError: except IndexError:
genre = "Unknown" genre = "Unknown"
try:
date = audio['date'][0]
except KeyError:
try:
date = audio['TDRC'][0]
except:
date = "Unknown"
except IndexError:
date = "Unknown"
img_path = extract_thumb(full_path) img_path = extract_thumb(full_path)
length = str(datetime.timedelta(seconds=round(audio.info.length)))
if length[:2] == "0:":
length = length.replace('0:', '')
tags = { tags = {
"filepath": full_path.replace(helpers.home_dir, ''), "filepath": full_path.replace(helpers.home_dir, ''),
"folder": os.path.dirname(full_path).replace(helpers.home_dir, ""), "folder": os.path.dirname(full_path).replace(helpers.home_dir, ""),
@ -208,8 +226,9 @@ def getTags(full_path: str) -> dict:
"album_artist": album_artist, "album_artist": album_artist,
"album": album, "album": album,
"genre": genre, "genre": genre,
"length": round(audio.info.length), "length": length,
"bitrate": audio.info.bitrate, "bitrate": round(int(audio.info.bitrate)/1000),
"date": str(date)[:4],
"image": img_path, "image": img_path,
} }
@ -228,7 +247,8 @@ def getAlbumBio(title: str, album_artist: str) -> dict:
return "None" return "None"
try: 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: except KeyError:
bio = None bio = None
@ -236,3 +256,20 @@ def getAlbumBio(title: str, album_artist: str) -> dict:
return "None" return "None"
return bio return bio
def create_track_class(tags):
return models.Track(
tags['_id']["$oid"],
tags['title'],
tags['artists'],
tags['album_artist'],
tags['album'],
tags['filepath'],
tags['folder'],
tags['length'],
tags['date'],
tags['genre'],
tags['bitrate'],
tags['image']
)

View File

@ -17,6 +17,7 @@ home_dir = os.path.expanduser('~') + '/'
app_dir = os.path.join(home_dir, '.musicx') app_dir = os.path.join(home_dir, '.musicx')
last_fm_api_key = "762db7a44a9e6fb5585661f5f2bdf23a" last_fm_api_key = "762db7a44a9e6fb5585661f5f2bdf23a"
def background(f): def background(f):
''' '''
a threading decorator a threading decorator
@ -29,6 +30,9 @@ def background(f):
@background @background
def check_for_new_songs(): def check_for_new_songs():
"""
Checks for new songs every 5 minutes.
"""
flag = False flag = False
while flag is False: while flag is False:
@ -68,12 +72,12 @@ def remove_duplicates(array: list) -> list:
while song_num < len(array) - 1: while song_num < len(array) - 1:
for index, song in enumerate(array): for index, song in enumerate(array):
try: if array[song_num].title == song.title and \
array[song_num].album == song.album and \
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[song_num].artists == song.artists and \
index != song_num:
array.remove(song) array.remove(song)
except:
print('whe')
song_num += 1 song_num += 1
return array return array
@ -113,27 +117,29 @@ def create_config_dir() -> None:
for dir in dirs: for dir in dirs:
path = os.path.join(config_folder, dir) path = os.path.join(config_folder, dir)
os.chmod(path, 0o755)
try: try:
os.makedirs(path) os.makedirs(path)
except FileExistsError: except FileExistsError:
pass pass
os.chmod(path, 0o755)
def getAllSongs() -> None: def getAllSongs() -> None:
""" """
Gets all songs under the ~/ directory. Gets all songs under the ~/ directory.
""" """
tracks = instances.songs_instance.get_all_songs() tracks = []
for track in instances.songs_instance.get_all_songs():
track = functions.create_track_class(track)
for track in tracks:
try: try:
os.chmod(os.path.join(home_dir, track['filepath']), 0o755) os.chmod(os.path.join(home_dir, track.filepath), 0o755)
except FileNotFoundError: except FileNotFoundError:
instances.songs_instance.remove_song_by_filepath(track['filepath']) instances.songs_instance.remove_song_by_filepath(track.filepath)
if track['artists'] is not None: tracks.append(track)
track['artists'] = track['artists'].split(', ')
return tracks return tracks

View File

@ -1,3 +1,4 @@
from dataclasses import dataclass
import pymongo import pymongo
import json import json
from bson import ObjectId, json_util from bson import ObjectId, json_util
@ -119,3 +120,25 @@ class AllSongs(Mongo):
return True return True
except: except:
return False return False
@dataclass
class Track:
id: str
title: str
artists: str
album_artist: str
album: str
filepath: str
folder: str
length: int
date: int
genre: str
bitrate: int
image: str
def __post_init__(self):
self.artists = self.artists.split(', ')
self.image = "http://127.0.0.1:8900/images/thumbnails/" + self.image

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="folder"> <div class="folder">
<div class="table rounded" v-if="songs.length"> <div class="table rounded" v-if="props.songs.length">
<table> <table>
<thead> <thead>
<tr> <tr>
@ -12,7 +12,7 @@
</thead> </thead>
<tbody> <tbody>
<SongItem <SongItem
v-for="song in songs" v-for="song in props.songs"
:key="song" :key="song"
:song="song" :song="song"
@updateQueue="updateQueue" @updateQueue="updateQueue"
@ -21,42 +21,43 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div v-else-if="songs.length === 0 && search_query"> <div v-else-if="props.songs.length === 0 && search_query">
<div class="no-results"> <div class="no-results">
<div class="icon"></div> <div class="icon"></div>
<div class="text"> Track not found!</div> <div class="text">No tracks 🎸</div>
</div> </div>
</div> </div>
<div v-else ref="songtitle"></div> <div v-else ref="songtitle"></div>
</div> </div>
</template> </template>
<script> <script setup>
import { ref } from "@vue/reactivity"; import { ref } from "@vue/reactivity";
import { onMounted } from "@vue/runtime-core"; import { onMounted } from "@vue/runtime-core";
// import { defineProps } from 'vue';
import SongItem from "../shared/SongItem.vue"; import SongItem from "../shared/SongItem.vue";
import routeLoader from "@/composables/routeLoader.js"; import routeLoader from "@/composables/routeLoader.js";
import perks from "@/composables/perks.js"; import perks from "@/composables/perks.js";
import state from "@/composables/state.js"; import state from "@/composables/state.js";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
export default { const props = defineProps({
props: ["songs"], songs: {
components: { type: Array,
SongItem, required: true
}, }
setup() { });
let route;
const current = ref(perks.current);
const search_query = ref(state.search_query);
onMounted(() => { let route;
const search_query = ref(state.search_query);
onMounted(() => {
route = useRoute().name; route = useRoute().name;
}); });
function updateQueue(song) { function updateQueue(song) {
let type; let type;
switch (route) { switch (route) {
@ -70,20 +71,11 @@ export default {
} }
perks.updateQueue(song, type); perks.updateQueue(song, type);
} }
function loadAlbum(title, album_artist) { function loadAlbum(title, album_artist) {
routeLoader.toAlbum(title, album_artist); routeLoader.toAlbum(title, album_artist);
} }
return {
updateQueue,
loadAlbum,
current,
search_query,
};
},
};
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -21,7 +21,7 @@
</div> </div>
</div> </div>
<div class="progress"> <div class="progress">
<div class="duration">{{ fmtMSS(current.length) }}</div> <div class="duration">{{ current.length }}</div>
<input <input
id="progress" id="progress"
type="range" type="range"

View File

@ -31,7 +31,7 @@
:key="song" :key="song"
@click="playThis(song)" @click="playThis(song)"
:class="{ :class="{
currentInQueue: current._id.$oid == song._id.$oid, currentInQueue: current.id == song.id,
}" }"
> >
<div <div
@ -42,7 +42,7 @@
> >
<div <div
class="now-playing-track image" class="now-playing-track image"
v-if="current._id.$oid == song._id.$oid" v-if="current.id == song.id"
:class="{ active: is_playing, not_active: !is_playing }" :class="{ active: is_playing, not_active: !is_playing }"
></div> ></div>
</div> </div>

View File

@ -1,8 +1,8 @@
<template> <template>
<tr <tr
class="songlist-item" class="songlist-item"
:class="{ current: current.id == song.id }"
> >
<!-- :class="{ current: current._id.$oid == song._id.$oid }" -->
<td class="flex" @click="emitUpdate(song)"> <td class="flex" @click="emitUpdate(song)">
<div <div
class="album-art rounded image" class="album-art rounded image"
@ -12,7 +12,7 @@
> >
<div <div
class="now-playing-track image" class="now-playing-track image"
v-if="current._id.$oid == song._id.$oid" v-if="current.id == song.id"
:class="{ active: is_playing, not_active: !is_playing }" :class="{ active: is_playing, not_active: !is_playing }"
></div> ></div>
</div> </div>
@ -45,7 +45,7 @@
</div> </div>
</td> </td>
<td class="song-duration"> <td class="song-duration">
{{ `${Math.trunc(song.length / 60)} min` }} {{ song.length }}
</td> </td>
</tr> </tr>
</template> </template>

View File

@ -38,7 +38,7 @@ const putCommas = (artists) => {
function updateNext(song_) { function updateNext(song_) {
const index = state.queue.value.findIndex( const index = state.queue.value.findIndex(
(item) => item._id.$oid === song_._id.$oid (item) => item.id === song_.id
); );
if (index == queue.value.length - 1) { if (index == queue.value.length - 1) {
@ -53,7 +53,7 @@ function updateNext(song_) {
function updatePrev(song) { function updatePrev(song) {
const index = state.queue.value.findIndex( const index = state.queue.value.findIndex(
(item) => item._id.$oid === song._id.$oid (item) => item.id === song.id
); );
if (index == 0) { if (index == 0) {
@ -94,7 +94,7 @@ const updateQueue = async (song, type) => {
break; break;
} }
if (state.queue.value[0]._id.$oid !==list[0]._id.$oid) { if (state.queue.value[0].id !==list[0].id) {
const new_queue =list; const new_queue =list;
localStorage.setItem("queue", JSON.stringify(new_queue)); localStorage.setItem("queue", JSON.stringify(new_queue));
state.queue.value = new_queue; state.queue.value = new_queue;

View File

@ -46,7 +46,7 @@ const routes = [
]; ];
const router = createRouter({ const router = createRouter({
history: createWebHistory(process.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes, routes,
}); });

19
vite.config.js Normal file
View File

@ -0,0 +1,19 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
const path = require("path");
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
},
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/assets/css/_variables.scss";`,
},
},
},
});

View File

@ -1,9 +0,0 @@
module.exports = {
css: {
loaderOptions: {
sass: {
additionalData: `@import "@/assets/css/_variables.scss";`
}
}
}
};

8578
yarn.lock

File diff suppressed because it is too large Load Diff