diff --git a/backend/src/app.ts b/backend/src/app.ts
index 8d4fd61..a0488e8 100644
--- a/backend/src/app.ts
+++ b/backend/src/app.ts
@@ -29,9 +29,8 @@ const run = () => {
__dirname,
'/config/apple-music-api.config.secret.json'
) ).toString() );
-
- app.get( '/dev-token', ( request: express.Request, response: express.Response ) => {
- // sign dev token
+ app.get( '/dev-token', ( _request: express.Request, response: express.Response ) => {
+ // sign dev token
const now = new Date().getTime();
const tomorrow = now + ( 24 * 3600 * 1000 );
const jwtToken = jwt.sign( {
diff --git a/web/index.html b/web/index.html
index 9e5fc8f..cdd27f9 100644
--- a/web/index.html
+++ b/web/index.html
@@ -1,13 +1,18 @@
-
-
+
+
+
- Vite App
-
-
+ MusicPlayer
+
+
+
+
+
-
+
+
diff --git a/web/src/components/AddSong.vue b/web/src/components/AddSong.vue
index b4c1646..a734b70 100644
--- a/web/src/components/AddSong.vue
+++ b/web/src/components/AddSong.vue
@@ -1,4 +1,5 @@
+
+
+
+
diff --git a/web/src/composables/ImportTypePicker.vue b/web/src/composables/ImportTypePicker.vue
index e69de29..c6029d7 100644
--- a/web/src/composables/ImportTypePicker.vue
+++ b/web/src/composables/ImportTypePicker.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+ What would you like to add?
+
+
+ {{ source.name }}
+
+
+
+
+
diff --git a/web/src/composables/SearchView.vue b/web/src/composables/SearchView.vue
new file mode 100644
index 0000000..bd91978
--- /dev/null
+++ b/web/src/composables/SearchView.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
+ Search {{ searchOpts?.name }}
+
+
+
add(index)">
+
![]()
+
{{ result.name }}
+
{{ result.artist }}
+
+
+
+ Searching...
+
+
+
+
+
+
diff --git a/web/src/composables/SongDiskLoader.vue b/web/src/composables/SongDiskLoader.vue
deleted file mode 100644
index e69de29..0000000
diff --git a/web/src/composables/SongSearch.vue b/web/src/composables/SongSearch.vue
deleted file mode 100644
index e69de29..0000000
diff --git a/web/src/composables/diskLoader.ts b/web/src/composables/diskLoader.ts
new file mode 100644
index 0000000..c4473ee
--- /dev/null
+++ b/web/src/composables/diskLoader.ts
@@ -0,0 +1,16 @@
+import {
+ type Ref,
+ ref
+} from 'vue';
+import type {
+ FileImport
+} from './importTypePicker';
+
+export const isShowingDiskLoader = ref( false );
+
+export const diskLoaderOpts: Ref = ref( null );
+
+export const openDiskLoaderInterface = ( options: FileImport ) => {
+ isShowingDiskLoader.value = true;
+ diskLoaderOpts.value = options;
+};
diff --git a/web/src/composables/importTypePicker.ts b/web/src/composables/importTypePicker.ts
index ab5557e..4308c0b 100644
--- a/web/src/composables/importTypePicker.ts
+++ b/web/src/composables/importTypePicker.ts
@@ -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;
- 'addSelected': ( index: number ) => Promise;
+ '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;
}
+
+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;
+};
diff --git a/web/src/composables/searchManager.ts b/web/src/composables/searchManager.ts
new file mode 100644
index 0000000..e02a828
--- /dev/null
+++ b/web/src/composables/searchManager.ts
@@ -0,0 +1,16 @@
+import {
+ type Ref,
+ ref
+} from 'vue';
+import type {
+ CloudImport
+} from './importTypePicker';
+
+export const isShowingSearchView = ref( false );
+
+export const searchOpts: Ref = ref( null );
+
+export const openSearchInterface = ( options: CloudImport ) => {
+ searchOpts.value = options;
+ isShowingSearchView.value = true;
+};
diff --git a/web/src/composables/songDiskLoader.ts b/web/src/composables/songDiskLoader.ts
deleted file mode 100644
index e69de29..0000000
diff --git a/web/src/composables/songSearch.ts b/web/src/composables/songSearch.ts
deleted file mode 100644
index e69de29..0000000
diff --git a/web/src/scss/components/queue.scss b/web/src/scss/components/queue.scss
new file mode 100644
index 0000000..f49ab3b
--- /dev/null
+++ b/web/src/scss/components/queue.scss
@@ -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);
+ }
+ }
+}
diff --git a/web/src/scss/components/search.scss b/web/src/scss/components/search.scss
new file mode 100644
index 0000000..ab19504
--- /dev/null
+++ b/web/src/scss/components/search.scss
@@ -0,0 +1,12 @@
+.search-results-wrapper {
+ width: 70vw;
+ height: 60vh;
+ overflow-x: hidden;
+ overflow-y: scroll;
+
+ >div {
+ >img {
+ width: 10%;
+ }
+ }
+}
diff --git a/web/src/scss/components/sortablelist.scss b/web/src/scss/components/sortablelist.scss
index a3495a6..3d4047d 100644
--- a/web/src/scss/components/sortablelist.scss
+++ b/web/src/scss/components/sortablelist.scss
@@ -14,6 +14,11 @@
}
>.movable {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+
&.moving {
position: fixed;
}
diff --git a/web/src/ts/dtype/global.d.ts b/web/src/ts/dtype/global.d.ts
new file mode 100644
index 0000000..a630514
--- /dev/null
+++ b/web/src/ts/dtype/global.d.ts
@@ -0,0 +1,7 @@
+import type {
+ MusicKitObject
+} from 'musickitjs-v3-types/MusicKit';
+
+declare global {
+ var MusicKit: MusicKitObject;
+}
diff --git a/web/src/ts/dtype/playlist.d.ts b/web/src/ts/dtype/playlist.d.ts
index 9ae86b2..ae734ff 100644
--- a/web/src/ts/dtype/playlist.d.ts
+++ b/web/src/ts/dtype/playlist.d.ts
@@ -40,7 +40,7 @@ export interface Song {
/**
* The cover image as a URL
*/
- 'cover': string;
+ 'artwork': string;
/**
* Song duration
diff --git a/web/src/ts/player/index.ts b/web/src/ts/player/index.ts
index eaf556c..fe82458 100644
--- a/web/src/ts/player/index.ts
+++ b/web/src/ts/player/index.ts
@@ -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,
diff --git a/web/src/ts/player/plugins/interface.d.ts b/web/src/ts/player/plugins/interface.d.ts
index a96b9d4..dd3fa88 100644
--- a/web/src/ts/player/plugins/interface.d.ts
+++ b/web/src/ts/player/plugins/interface.d.ts
@@ -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;
+ 'addSongsFromThisSource': ( cb: ( songs: Song[] ) => void ) => void;
}
diff --git a/web/src/ts/player/plugins/musickit/index.ts b/web/src/ts/player/plugins/musickit/index.ts
index ccb9821..a89b9d1 100644
--- a/web/src/ts/player/plugins/musickit/index.ts
+++ b/web/src/ts/player/plugins/musickit/index.ts
@@ -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 => {
return new Promise( ( resolve, reject ) => {
const init = async ( storefront: string ): Promise => {
- 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
}
diff --git a/web/src/ts/player/plugins/musickit/search/albums.ts b/web/src/ts/player/plugins/musickit/search/albums.ts
deleted file mode 100644
index b65a462..0000000
--- a/web/src/ts/player/plugins/musickit/search/albums.ts
+++ /dev/null
@@ -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();
- };
-};
diff --git a/web/src/ts/player/plugins/musickit/search/dtype.d.ts b/web/src/ts/player/plugins/musickit/search/dtype.d.ts
new file mode 100644
index 0000000..d0ec258
--- /dev/null
+++ b/web/src/ts/player/plugins/musickit/search/dtype.d.ts
@@ -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;
+ }
+}
diff --git a/web/src/ts/player/plugins/musickit/search/index.ts b/web/src/ts/player/plugins/musickit/search/index.ts
index 6b24781..00eec1d 100644
--- a/web/src/ts/player/plugins/musickit/search/index.ts
+++ b/web/src/ts/player/plugins/musickit/search/index.ts
@@ -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 => {
- return [];
+export const addFromAppleMusic = async ( instance: MusicKitInstance, cb: ( songs: Song[] ) => void ): Promise => {
+ 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
+ }
+ ] );
};
diff --git a/web/src/ts/player/plugins/musickit/search/playlists.ts b/web/src/ts/player/plugins/musickit/search/playlists.ts
index 0e3c5bf..aa74732 100644
--- a/web/src/ts/player/plugins/musickit/search/playlists.ts
+++ b/web/src/ts/player/plugins/musickit/search/playlists.ts
@@ -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 => {
// TODO: Load the playlist content and return songs
- Promise.resolve();
+ return [];
};
};
diff --git a/web/src/ts/player/plugins/musickit/search/songs.ts b/web/src/ts/player/plugins/musickit/search/songs.ts
index 02b5625..6d592ce 100644
--- a/web/src/ts/player/plugins/musickit/search/songs.ts
+++ b/web/src/ts/player/plugins/musickit/search/songs.ts
@@ -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 => {
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
};
};
diff --git a/web/src/ts/player/state.ts b/web/src/ts/player/state.ts
index df288c2..cabc1d5 100644
--- a/web/src/ts/player/state.ts
+++ b/web/src/ts/player/state.ts
@@ -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 = 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();
diff --git a/web/todo.md b/web/todo.md
index bd7af45..d92dc15 100644
--- a/web/todo.md
+++ b/web/todo.md
@@ -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
diff --git a/web/tsconfig.node.json b/web/tsconfig.node.json
index c9b2bad..3dac890 100644
--- a/web/tsconfig.node.json
+++ b/web/tsconfig.node.json
@@ -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"
+ }
}