mirror of
https://github.com/janishutz/MusicPlayer.git
synced 2026-09-10 14:25:24 +02:00
feat: add songs via Apple Music
This commit is contained in:
+11
-6
@@ -1,13 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<title>MusicPlayer</title>
|
||||
<script src="https://js-cdn.music.apple.com/musickit/v3/musickit.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<noscript>This application requires JavaScript to work!</noscript>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import ImportTypePicker from '@/composables/ImportTypePicker.vue';
|
||||
import PopupElement from '@/components/PopupElement.vue';
|
||||
import player from '@/ts/player';
|
||||
import {
|
||||
@@ -17,6 +18,7 @@
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ImportTypePicker />
|
||||
<PopupElement v-model="showPopup" show-close>
|
||||
<h1>Add Song</h1>
|
||||
<div class="song-sources">
|
||||
|
||||
@@ -79,5 +79,5 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/scss/components/player.scss';
|
||||
@use '@/scss/components/player.scss';
|
||||
</style>
|
||||
|
||||
@@ -50,5 +50,5 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/scss/components/progressbar.scss';
|
||||
@use '@/scss/components/progressbar.scss';
|
||||
</style>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
} from 'vue';
|
||||
|
||||
const queue = player.queue;
|
||||
const idx = player.queueIdx;
|
||||
const queueIndex = player.queueIdx;
|
||||
const isPlaying = player.isPlaying;
|
||||
const showAddSong = ref( false );
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
Add
|
||||
</button>
|
||||
<button>
|
||||
<button @click="player.clearQueue">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
Clear
|
||||
</button>
|
||||
@@ -43,33 +43,46 @@
|
||||
<AddSong v-model="showAddSong" />
|
||||
<div>
|
||||
<SortableList v-slot="{ item: song, index }" v-model="queue">
|
||||
<div class="song-cover-wrapper">
|
||||
<img
|
||||
v-if="song.cover"
|
||||
:src="song.cover"
|
||||
alt="Song cover"
|
||||
class="song-cover"
|
||||
>
|
||||
<i v-else class="fa-solid fa-music song-cover"></i>
|
||||
<div v-if="isPlaying && index === idx" class="playing-symbols">
|
||||
<div id="bar-1" class="playing-bar"></div>
|
||||
<div id="bar-2" class="playing-bar"></div>
|
||||
<div id="bar-3" class="playing-bar"></div>
|
||||
<div class="song-list-element">
|
||||
<div class="song-cover-wrapper">
|
||||
<img
|
||||
v-if="song.artwork"
|
||||
:src="song.artwork"
|
||||
alt="Song cover"
|
||||
class="song-cover"
|
||||
>
|
||||
<i v-else class="fa-solid fa-music song-cover"></i>
|
||||
<div v-if="index === queueIndex" class="play-overlay">
|
||||
<div v-if="isPlaying" class="playing-symbols">
|
||||
<div id="bar-1" class="playing-bar"></div>
|
||||
<div id="bar-2" class="playing-bar"></div>
|
||||
<div id="bar-3" class="playing-bar"></div>
|
||||
</div>
|
||||
<i
|
||||
v-else
|
||||
class="fa-solid fa-pause"
|
||||
></i>
|
||||
</div>
|
||||
<div v-else class="play-overlay hover">
|
||||
<i class="fa-solid fa-play" @click="() => player.playIndex( index )"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3>{{ song.name }}</h3>
|
||||
<p>{{ song.artist }}</p>
|
||||
<p>{{ beautifyTime( song.duration ) }}</p>
|
||||
<p>{{ song['additional-info'] }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<i v-if="index !== queueIndex" class="fa-solid fa-trash-can"></i>
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
</div>
|
||||
<i v-else class="fa-solid fa-play play-pause" @click="() => player.playIndex( index )"></i>
|
||||
<i class="fa-solid fa-pause play-pause" @click="player.pause"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3>{{ song.name }}</h3>
|
||||
<p>{{ song.artist }}</p>
|
||||
<p>{{ beautifyTime( song.duration ) }}</p>
|
||||
<p>{{ song['additional-info'] }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<i class="fa-solid fa-trash-can"></i>
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
</div>
|
||||
</SortableList>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '@/scss/components/queue.scss';
|
||||
</style>
|
||||
|
||||
@@ -95,9 +95,10 @@
|
||||
const before = array.value.slice( 0, movingIdx.value );
|
||||
const after = array.value.slice( movingIdx.value + 1 );
|
||||
const el = array.value[ movingIdx.value ]!;
|
||||
const arr = before.concat( after );
|
||||
|
||||
array.value = before.concat( after );
|
||||
array.value.splice( movingCurrentIdx.value, 0, el );
|
||||
arr.splice( movingCurrentIdx.value, 0, el );
|
||||
array.value = arr;
|
||||
movingIdx.value = -1;
|
||||
movingCurrentIdx.value = -1;
|
||||
|
||||
@@ -156,5 +157,5 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/scss/components/sortablelist.scss'
|
||||
@use '@/scss/components/sortablelist.scss'
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import PopupElement from '@/components/PopupElement.vue';
|
||||
import {
|
||||
isShowingDiskLoader
|
||||
} from './diskLoader';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<PopupElement v-model="isShowingDiskLoader" show-close>
|
||||
<h3>Load from disk</h3>
|
||||
</PopupElement>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
type ImportTypes,
|
||||
importers,
|
||||
isShowingImportTypePicker
|
||||
} from './importTypePicker';
|
||||
import PopupElement from '@/components/PopupElement.vue';
|
||||
import SearchView from './SearchView.vue';
|
||||
import {
|
||||
openDiskLoaderInterface
|
||||
} from './diskLoader';
|
||||
import {
|
||||
openSearchInterface
|
||||
} from './searchManager';
|
||||
|
||||
const openPicker = ( source: ImportTypes ) => {
|
||||
isShowingImportTypePicker.value = false;
|
||||
|
||||
|
||||
if ( source.type === 'cloud' ) {
|
||||
openSearchInterface( source );
|
||||
} else {
|
||||
openDiskLoaderInterface( source );
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<SearchView />
|
||||
<PopupElement v-model="isShowingImportTypePicker">
|
||||
<h3>What would you like to add?</h3>
|
||||
<div class="import-type-list">
|
||||
<div v-for="(source, index) in importers" :key="index" @click="openPicker( source )">
|
||||
{{ source.name }}
|
||||
</div>
|
||||
</div>
|
||||
</PopupElement>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
type Ref,
|
||||
ref
|
||||
} from 'vue';
|
||||
import {
|
||||
isShowingSearchView,
|
||||
searchOpts
|
||||
} from './searchManager';
|
||||
import PopupElement from '@/components/PopupElement.vue';
|
||||
import type {
|
||||
Song
|
||||
} from '@/ts/dtype/playlist';
|
||||
|
||||
const results: Ref<Song[]> = ref( [] );
|
||||
const query = ref( '' );
|
||||
const isSearching = ref( false );
|
||||
|
||||
let timeout = -1;
|
||||
let searchedForQuery = '';
|
||||
|
||||
const search = ( ev: KeyboardEvent ) => {
|
||||
if ( ev.key === 'Enter' ) {
|
||||
// TODO: If no new input, instead of searching, add first song
|
||||
try {
|
||||
clearTimeout( timeout );
|
||||
} catch { /* empty */ }
|
||||
|
||||
doSearch();
|
||||
} else if ( query.value.length > 2 ) {
|
||||
try {
|
||||
clearTimeout( timeout );
|
||||
} catch { /* empty */ }
|
||||
|
||||
timeout = setTimeout( doSearch, 250 );
|
||||
}
|
||||
};
|
||||
|
||||
const doSearch = async () => {
|
||||
if ( query.value === searchedForQuery ) return;
|
||||
|
||||
isSearching.value = true;
|
||||
searchedForQuery = query.value;
|
||||
results.value = await searchOpts.value?.search( query.value ) ?? [];
|
||||
isSearching.value = false;
|
||||
};
|
||||
|
||||
const add = ( idx: number ) => {
|
||||
searchOpts.value?.addSelected( idx );
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<PopupElement v-model="isShowingSearchView" show-close>
|
||||
<h2>Search {{ searchOpts?.name }}</h2>
|
||||
<input
|
||||
v-model="query"
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
@keypress="search"
|
||||
>
|
||||
<div v-if="!isSearching" class="search-results-wrapper">
|
||||
<div v-for="(result, index) in results" :key="index" @click="() => add(index)">
|
||||
<img :src="result.artwork" :alt="'Album artwork of ' + result.name + ' by ' + result.artist">
|
||||
<h4>{{ result.name }}</h4>
|
||||
<p>{{ result.artist }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="search-results-wrapper">
|
||||
Searching...
|
||||
</div>
|
||||
</PopupElement>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '@/scss/components/search.scss';
|
||||
</style>
|
||||
@@ -0,0 +1,16 @@
|
||||
import {
|
||||
type Ref,
|
||||
ref
|
||||
} from 'vue';
|
||||
import type {
|
||||
FileImport
|
||||
} from './importTypePicker';
|
||||
|
||||
export const isShowingDiskLoader = ref( false );
|
||||
|
||||
export const diskLoaderOpts: Ref<FileImport | null> = ref( null );
|
||||
|
||||
export const openDiskLoaderInterface = ( options: FileImport ) => {
|
||||
isShowingDiskLoader.value = true;
|
||||
diskLoaderOpts.value = options;
|
||||
};
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
type Ref,
|
||||
ref
|
||||
} from 'vue';
|
||||
import type {
|
||||
Song
|
||||
} from '@/ts/dtype/playlist';
|
||||
@@ -5,13 +9,25 @@ import type {
|
||||
export interface CloudImport {
|
||||
// TODO: Consider if should make selectable and display album / playlist contents to select specific songs
|
||||
'name': string;
|
||||
'kind': 'list' | 'nested-list';
|
||||
'type': 'cloud';
|
||||
'search': ( term: string ) => Promise<Song[]>;
|
||||
'addSelected': ( index: number ) => Promise<Song[]>;
|
||||
'addSelected': ( index: number ) => void;
|
||||
}
|
||||
|
||||
// TODO: Need to add a way to access Apple Music API everywhere (maybe configure MusicKitJS globally and consume it in its plugin later?)
|
||||
export interface FileImport {
|
||||
'name': string;
|
||||
'type': 'file';
|
||||
'process': ( files: File[] ) => Promise<Song[]>;
|
||||
}
|
||||
|
||||
export type ImportTypes = CloudImport | FileImport;
|
||||
|
||||
export const isShowingImportTypePicker = ref( false );
|
||||
|
||||
export const importers: Ref<( CloudImport | FileImport )[]> = ref( [] );
|
||||
|
||||
export const openImportTypePicker = ( options: ( CloudImport | FileImport )[] ) => {
|
||||
isShowingImportTypePicker.value = true;
|
||||
importers.value = options;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import {
|
||||
type Ref,
|
||||
ref
|
||||
} from 'vue';
|
||||
import type {
|
||||
CloudImport
|
||||
} from './importTypePicker';
|
||||
|
||||
export const isShowingSearchView = ref( false );
|
||||
|
||||
export const searchOpts: Ref<CloudImport | null> = ref( null );
|
||||
|
||||
export const openSearchInterface = ( options: CloudImport ) => {
|
||||
searchOpts.value = options;
|
||||
isShowingSearchView.value = true;
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
.song-list-element {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
>.song-cover-wrapper {
|
||||
height: 10rem;
|
||||
width: 10rem;
|
||||
position: relative;
|
||||
|
||||
.song-cover,
|
||||
.fa-solid {
|
||||
height: 10rem;
|
||||
width: 10rem;
|
||||
font-size: 7rem;
|
||||
line-height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
>.play-overlay {
|
||||
background-color: var(--overlay-color);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&.hover {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
>.play-overlay.hover {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.playing-symbols {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
margin: 0;
|
||||
padding-left: 20%;
|
||||
padding-right: 20%;
|
||||
width: 60%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
|
||||
.playing-bar {
|
||||
height: 40%;
|
||||
background-color: white;
|
||||
width: 12%;
|
||||
border-radius: 50px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#bar-1 {
|
||||
animation: music-playing 0.9s infinite ease-in-out;
|
||||
}
|
||||
|
||||
#bar-2 {
|
||||
animation: music-playing 0.9s infinite ease-in-out;
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
#bar-3 {
|
||||
animation: music-playing 0.9s infinite ease-in-out;
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
|
||||
@keyframes music-playing {
|
||||
0% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scaleY(0.5);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
.search-results-wrapper {
|
||||
width: 70vw;
|
||||
height: 60vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
|
||||
>div {
|
||||
>img {
|
||||
width: 10%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,11 @@
|
||||
}
|
||||
|
||||
>.movable {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.moving {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
import type {
|
||||
MusicKitObject
|
||||
} from 'musickitjs-v3-types/MusicKit';
|
||||
|
||||
declare global {
|
||||
var MusicKit: MusicKitObject;
|
||||
}
|
||||
Vendored
+1
-1
@@ -40,7 +40,7 @@ export interface Song {
|
||||
/**
|
||||
* The cover image as a URL
|
||||
*/
|
||||
'cover': string;
|
||||
'artwork': string;
|
||||
|
||||
/**
|
||||
* Song duration
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
addSongList,
|
||||
clearQueue,
|
||||
shuffleList
|
||||
} from './playlists/add';
|
||||
import {
|
||||
@@ -117,7 +118,7 @@ const getSources = (): string[] => {
|
||||
};
|
||||
|
||||
const addSongFromSource = async ( source: string ) => {
|
||||
addSongList( await sources[source]!.addSongsFromThisSource() );
|
||||
sources[source]!.addSongsFromThisSource( addSongList );
|
||||
};
|
||||
|
||||
export default {
|
||||
@@ -133,6 +134,7 @@ export default {
|
||||
addSongFromSource,
|
||||
next,
|
||||
prev,
|
||||
clearQueue,
|
||||
queue,
|
||||
queueIdx,
|
||||
duration,
|
||||
|
||||
+1
-1
@@ -94,5 +94,5 @@ export interface PlayerSourcePlugin {
|
||||
* Called when user adds another song to the playlist via this source.
|
||||
* You may use the provided interface elements (search bar and popups) to e.g. ask if user wants to use a playlist, album, etc
|
||||
*/
|
||||
'addSongsFromThisSource': () => Promise<Song[]>;
|
||||
'addSongsFromThisSource': ( cb: ( songs: Song[] ) => void ) => void;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'musickitjs-v3-types';
|
||||
import type {
|
||||
PlayerSourcePlugin,
|
||||
PlayerSourcePluginInitializer
|
||||
} from '../interface';
|
||||
import type {
|
||||
Song
|
||||
} from '@/ts/dtype/playlist';
|
||||
import {
|
||||
addFromAppleMusic
|
||||
} from './search';
|
||||
@@ -13,7 +15,7 @@ import {
|
||||
export const useMusicKit: PlayerSourcePluginInitializer = ( storefront: string = 'ch' ): Promise<PlayerSourcePlugin> => {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
const init = async ( storefront: string ): Promise<PlayerSourcePlugin> => {
|
||||
const res = await fetch( import.meta.env.VITE_BACKEND_URL + '/', {
|
||||
const res = await fetch( import.meta.env.VITE_BACKEND_URL + '/dev-token', {
|
||||
'credentials': 'include'
|
||||
} );
|
||||
|
||||
@@ -23,8 +25,8 @@ export const useMusicKit: PlayerSourcePluginInitializer = ( storefront: string =
|
||||
'developerToken': token,
|
||||
'app': {
|
||||
'name': 'MusicPlayer',
|
||||
'build': '4',
|
||||
'icon': 'https://music.janishutz.com/logo.jpg'
|
||||
'build': '4'
|
||||
// 'icon': 'https://music.janishutz.com/logo.jpg'
|
||||
},
|
||||
'storefrontId': storefront.toUpperCase()
|
||||
} );
|
||||
@@ -51,8 +53,7 @@ export const useMusicKit: PlayerSourcePluginInitializer = ( storefront: string =
|
||||
'name': 'Apple Music',
|
||||
'seekTo': controls.seekTo,
|
||||
'login': login,
|
||||
// TODO: Implement
|
||||
'addSongsFromThisSource': async () => await addFromAppleMusic(),
|
||||
'addSongsFromThisSource': ( cb: ( songs: Song[] ) => void ) => addFromAppleMusic( instance, cb ),
|
||||
'loading': {
|
||||
'requiresLocalFiles': false
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import type {
|
||||
MusicKitInstance
|
||||
} from 'musickitjs-v3-types/MusicKitInstance';
|
||||
|
||||
export const searchAlbums = async ( instance: MusicKitInstance, term: string ) => {
|
||||
const addSelected = ( idx: number ) => {
|
||||
// TODO: Load album's songs and return them
|
||||
Promise.resolve();
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import type {
|
||||
Artwork
|
||||
} from 'musickitjs-v3-types/enums';
|
||||
|
||||
export interface AppleMusicApiSearchResult {
|
||||
'results': {
|
||||
'songs': {
|
||||
'data': AppleMusicSongData[],
|
||||
'href': string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface AppleMusicSongData {
|
||||
'id': string,
|
||||
'type': string;
|
||||
'href': string;
|
||||
'attributes': {
|
||||
'albumName': string;
|
||||
'artistName': string;
|
||||
'artwork': Artwork,
|
||||
'name': string;
|
||||
'genreNames': string[];
|
||||
'durationInMillis': number;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,28 @@ import type {
|
||||
import type {
|
||||
Song
|
||||
} from '@/ts/dtype/playlist';
|
||||
import {
|
||||
openImportTypePicker
|
||||
} from '@/composables/importTypePicker';
|
||||
import {
|
||||
searchSongs
|
||||
} from './songs';
|
||||
|
||||
export const addFromAppleMusic = async ( instance: MusicKitInstance ): Promise<Song[]> => {
|
||||
return [];
|
||||
export const addFromAppleMusic = async ( instance: MusicKitInstance, cb: ( songs: Song[] ) => void ): Promise<void> => {
|
||||
const songs = await searchSongs( instance, cb );
|
||||
|
||||
openImportTypePicker( [
|
||||
{
|
||||
'name': 'Songs',
|
||||
'type': 'cloud',
|
||||
'addSelected': songs.addSelected,
|
||||
'search': songs.search
|
||||
},
|
||||
{
|
||||
'name': 'Playlists',
|
||||
'type': 'cloud',
|
||||
'addSelected': songs.addSelected,
|
||||
'search': songs.search
|
||||
}
|
||||
] );
|
||||
};
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import type {
|
||||
Song
|
||||
} from '@/ts/dtype/playlist';
|
||||
import type {
|
||||
MusicKitInstance
|
||||
} from 'musickitjs-v3-types/MusicKitInstance';
|
||||
|
||||
export const searchPlaylists = async ( instance: MusicKitInstance, userPlaylists: boolean = true ) => {
|
||||
const addSelected = ( idx: number ) => {
|
||||
export const searchPlaylists = async ( instance: MusicKitInstance ) => {
|
||||
// TODO: Get all playlists
|
||||
|
||||
const search = async () => {};
|
||||
|
||||
const addSelected = async ( idx: number ): Promise<Song[]> => {
|
||||
// TODO: Load the playlist content and return songs
|
||||
Promise.resolve();
|
||||
return [];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,20 +1,46 @@
|
||||
import type {
|
||||
AppleMusicApiSearchResult
|
||||
} from './dtype';
|
||||
import type {
|
||||
MusicKitInstance
|
||||
} from 'musickitjs-v3-types/MusicKitInstance';
|
||||
import type {
|
||||
Song
|
||||
} from '@/ts/dtype/playlist';
|
||||
|
||||
export const seachSongs = async ( instance: MusicKitInstance ) => {
|
||||
const search = async ( term: string ) => {
|
||||
export const searchSongs = async ( instance: MusicKitInstance, cb: ( songs: Song[] ) => void ) => {
|
||||
let songs: Song[] = [];
|
||||
|
||||
const search = async ( term: string ): Promise<Song[]> => {
|
||||
const params = {
|
||||
'term': term,
|
||||
'types': [ 'songs' ],
|
||||
'l': 'en-us'
|
||||
'types': [ 'songs' ]
|
||||
};
|
||||
const results = ( ( await instance.api.music( '/v1/catalog/{{storefrontId}}/search', params ) ).data as AppleMusicApiSearchResult ).results.songs.data;
|
||||
|
||||
await instance.api.music( '/v1/catalog/{{storefrontId}}/search', params );
|
||||
songs = [];
|
||||
|
||||
for ( const result of results ) {
|
||||
songs.push( {
|
||||
'duration': Math.round( result.attributes.durationInMillis / 1000 ),
|
||||
'name': result.attributes.name,
|
||||
'additional-info': '',
|
||||
'artist': result.attributes.artistName,
|
||||
'artwork': window.MusicKit.formatArtworkURL( result.attributes.artwork, 1000, 1000 ),
|
||||
'identifier': result.id,
|
||||
'source': 'applemusic'
|
||||
} );
|
||||
}
|
||||
|
||||
return songs;
|
||||
};
|
||||
|
||||
const addSelected = ( idx: number ) => {
|
||||
// TODO: Need to only return the selected song
|
||||
Promise.resolve();
|
||||
const addSelected = async ( idx: number ) => {
|
||||
cb( [ songs[idx]! ] );
|
||||
};
|
||||
|
||||
return {
|
||||
search,
|
||||
addSelected
|
||||
};
|
||||
};
|
||||
|
||||
@@ -11,6 +11,12 @@ import type {
|
||||
import type {
|
||||
RepeatMode
|
||||
} from '../dtype/player';
|
||||
import {
|
||||
useLocalPlayer
|
||||
} from './plugins/local';
|
||||
import {
|
||||
useMusicKit
|
||||
} from './plugins/musickit';
|
||||
|
||||
export const sources: {
|
||||
[key: string]: PlayerSourcePlugin
|
||||
@@ -29,3 +35,19 @@ export const isPlaying = ref( false );
|
||||
export const shuffle = ref( false );
|
||||
|
||||
export const repeat: Ref<RepeatMode> = ref( 'off' );
|
||||
|
||||
const initSources = async () => {
|
||||
try {
|
||||
sources['applemusic'] = await useMusicKit();
|
||||
} catch {
|
||||
console.warn( 'MusicKitJS intialization failed' );
|
||||
}
|
||||
|
||||
try {
|
||||
sources['local'] = await useLocalPlayer();
|
||||
} catch {
|
||||
console.warn( 'Local Player intialization failed' );
|
||||
}
|
||||
};
|
||||
|
||||
initSources();
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
- [ ] About page
|
||||
- [ ] Apple-Music signin
|
||||
- [ ] Loading existing playlists with local music and association of that
|
||||
- [ ] Remote screens can use polling (once a minute or so) or SSE for updating (/fancy uses SSE by default, /share uses polling by default). Should reduce server load
|
||||
|
||||
# Backend
|
||||
- [ ] Implement all endpoints
|
||||
|
||||
+26
-24
@@ -1,27 +1,29 @@
|
||||
// TSConfig for modules that run in Node.js environment via either transpilation or type-stripping.
|
||||
{
|
||||
"extends": "@tsconfig/node24/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"playwright.config.*",
|
||||
"eslint.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
// Most tools use transpilation instead of Node.js's native type-stripping.
|
||||
// Bundler mode provides a smoother developer experience.
|
||||
"module": "preserve",
|
||||
"moduleResolution": "bundler",
|
||||
|
||||
// Include Node.js types and avoid accidentally including other `@types/*` packages.
|
||||
"types": ["node"],
|
||||
|
||||
// Disable emitting output during `vue-tsc --build`, which is used for type-checking only.
|
||||
"noEmit": true,
|
||||
|
||||
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
|
||||
// Specified here to keep it out of the root directory.
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
|
||||
}
|
||||
"extends": "@tsconfig/node24/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"playwright.config.*",
|
||||
"eslint.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
// Most tools use transpilation instead of Node.js's native type-stripping.
|
||||
// Bundler mode provides a smoother developer experience.
|
||||
"module": "preserve",
|
||||
"moduleResolution": "bundler",
|
||||
// Include Node.js types and avoid accidentally including other `@types/*` packages.
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"typeRoots": [
|
||||
"./src/ts/dtype/global.d.ts"
|
||||
],
|
||||
// Disable emitting output during `vue-tsc --build`, which is used for type-checking only.
|
||||
"noEmit": true,
|
||||
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
|
||||
// Specified here to keep it out of the root directory.
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user