diff --git a/package.json b/package.json index af970515..64b8ec00 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,8 @@ "dependencies": { "@sentry/electron": "^3.0.7", "@sentry/integrations": "^6.19.6", + "@types/pouchdb": "^6.4.0", + "@types/pouchdb-node": "^6.1.4", "adm-zip": "0.4.10", "airtunes2": "git+https://github.com/ciderapp/node_airtunes2", "castv2-client": "^1.2.0", @@ -58,16 +60,21 @@ "jimp": "^0.16.1", "jsonc": "^2.0.0", "lastfmapi": "^0.1.1", + "level": "^8.0.0", "mdns-js": "git+https://github.com/ciderapp/node-mdns-js.git", "mpris-service": "^2.1.2", "music-metadata": "^7.12.3", "node-gyp": "^9.0.0", "node-ssdp": "^4.0.1", + "pouchdb-adapter-leveldb": "^7.3.0", + "pouchdb-node": "^7.3.0", + "pouchdb-upsert": "^2.2.0", "qrcode": "^1.5.0", "react": "^18.0.0", "react-dom": "^18.0.0", "run-script-os": "^1.1.6", "source-map-support": "^0.5.21", + "ts-md5": "^1.2.11", "v8-compile-cache": "^2.3.0", "wallpaper": "5.0.1", "ws": "^8.5.0", diff --git a/src/main/base/browserwindow.ts b/src/main/base/browserwindow.ts index 747eb46d..c233fe5f 100644 --- a/src/main/base/browserwindow.ts +++ b/src/main/base/browserwindow.ts @@ -1,5 +1,5 @@ import { join } from "path"; -import { app, BrowserWindow as bw, ipcMain, ShareMenu, shell, screen } from "electron"; +import { app, BrowserWindow as bw, ipcMain, ShareMenu, shell, screen, dialog } from "electron"; import * as windowStateKeeper from "electron-window-state"; import * as express from "express"; import * as getPort from "get-port"; @@ -27,8 +27,7 @@ import { watch } from "chokidar"; import * as os from "os"; import wallpaper from "wallpaper"; import * as AdmZip from "adm-zip"; -import * as path from 'path'; -const { readdir } = require('fs').promises; +import { LocalFiles } from "../providers/local/"; /** @@ -40,11 +39,11 @@ const { readdir } = require('fs').promises; export class BrowserWindow { public static win: any | undefined = null; private devMode: boolean = !app.isPackaged; + public static express: any | undefined = null; private audioStream: any = new Stream.PassThrough(); private headerSent: any = false; private chromecastIP: any = []; - private localSongs: any = []; private clientPort: number = 0; private remotePort: number = 6942; private EnvironmentVariables: object = { @@ -121,6 +120,7 @@ export class BrowserWindow { "components/fullscreen", "components/miniplayer", "components/castmenu", + "components/pathmenu", "components/airplay-modal", "components/artist-chip", "components/hello-world", @@ -408,9 +408,10 @@ export class BrowserWindow { } // Start the webserver for the browser window to load - + // LocalFiles.DB.init() this.startWebServer(); + BrowserWindow.win = new bw(this.options); // cant be built in CI // if (process.platform === "win32" && (utils.getStoreValue('visual.transparent') ?? false)) { @@ -466,7 +467,7 @@ export class BrowserWindow { */ private startWebServer(): void { const app = express(); - + BrowserWindow.express = app; app.use(express.static(join(utils.getPath('srcPath'), "./renderer/"))); app.set("views", join(utils.getPath('srcPath'), "./renderer/views")); app.set("view engine", "ejs"); @@ -550,16 +551,6 @@ export class BrowserWindow { res.send(`// Theme not found - ${userThemePath}`); } }); - app.get("/ciderlocal/:songs", (req, res) => { - const audio = atob(req.params.songs.replace(/_/g, '/').replace(/-/g, '+')); - console.log('auss', audio) - let data = { - data: - this.localSongs.filter((f: any) => audio.split(',').includes(f.id)) - }; - res.send(data); - }); - app.get("/themes/:theme/*", (req: { params: { theme: string, 0: string } }, res) => { const theme = req.params.theme; @@ -623,6 +614,9 @@ export class BrowserWindow { utils.getWindow().webContents.send('setStoreValue', 'connectUser', JSON.parse(req.params.data)) res.redirect(`https://connect.cidercollective.dev/linked.html`) }); + + LocalFiles.setupHandlers() + // [Connect] Set auth URL in store for `shell.openExternal` utils.setStoreValue('cc_authURL', `https://connect.cidercollective.dev/callback/discord?app=cider&appPort=${this.clientPort}`) console.log(`[Connect] Auth URL: ${utils.getStoreValue('cc_authURL')}`) @@ -673,9 +667,9 @@ export class BrowserWindow { callback({ redirectURL: `http://localhost:${this.clientPort}/apple-hls.js`, }); - } else if (details.url.includes("ciderlocal")) { + } else if (details.url.includes("ciderlocal") && !details.url.includes("https://apic-desktop.musixmatch.com") ) { let text = details.url.toString().includes('ids=') ? decodeURIComponent(details.url.toString()).split("?ids=")[1] : decodeURIComponent(details.url.toString().substring(details.url.toString().lastIndexOf('/') + 1)); - console.log('localurl', text) + //console.log('localurl',text) callback({ redirectURL: `http://localhost:${this.clientPort}/ciderlocal/${Buffer.from(text).toString('base64url')}`, }); @@ -1190,103 +1184,13 @@ export class BrowserWindow { }); - ipcMain.on("scanLibrary", async (event, folders) => { - async function getFiles(dir: any) { - const dirents = await readdir(dir, { withFileTypes: true }); - const files = await Promise.all(dirents.map((dirent: any) => { - const res = path.resolve(dir, dirent.name); - return dirent.isDirectory() ? getFiles(res) : res; - })); - return Array.prototype.concat(...files); - } - if (folders == null || folders.length == null || folders.length == 0) folders = ["D:\\Music"] - console.log('folders', folders) - let files: any[] = [] - for (var folder of folders) { - // get files from the Music folder - files = files.concat(await getFiles(folder)) - } - - //console.log("cider.files", files2); - let supporttedformats = ["mp3", "aac", "webm", "flac", "m4a", "ogg", "wav", "opus"] - let audiofiles = files.filter(f => supporttedformats.includes(f.substring(f.lastIndexOf('.') + 1))); - // console.log("cider.files2", audiofiles, audiofiles.length); - let metadatalist = [] - let numid = 0; - for (var audio of audiofiles) { - try { - const metadata = await mm.parseFile(audio); - if (metadata != null) { - let form = { - "id": "ciderlocal" + numid, - "type": "podcast-episodes", - "href": audio, - "attributes": { - "artwork": { - "width": 3000, - "height": 3000, - "url": metadata.common.picture != undefined ? "data:image/png;base64," + metadata.common.picture[0].data.toString('base64') + "" : "", - }, - "topics": [], - "url": "", - "subscribable": true, - "mediaKind": "audio", - "genreNames": [ - "" - ], - // "playParams": { - // "id": "ciderlocal" + numid, - // "kind": "podcast", - // "isLibrary": true, - // "reporting": false }, - "trackNumber": metadata.common.track?.no ?? 0, - "discNumber": metadata.common.disk?.no ?? 0, - "name": metadata.common.title ?? audio.substring(audio.lastIndexOf('\\') + 1), - "albumName": metadata.common.album, - "artistName": metadata.common.artist, - "copyright": metadata.common.copyright ?? "", - "assetUrl": "file:///" + audio, - "contentAdvisory": "", - "releaseDateTime": "2022-05-13T00:23:00Z", - "durationInMilliseconds": Math.floor((metadata.format.duration ?? 0) * 1000), - - "offers": [ - { - "kind": "get", - "type": "STDQ" - } - ], - "contentRating": "clean" - } - }; - numid += 1; - - // let form = {"id": "/ciderlocal?" + audio, - // "type": "library-songs", - // "href": "/ciderlocal?" + audio, - // "artwork": { - // "url": metadata.common.picture != undefined ? "data:image/png;base64,"+metadata.common.picture[0].data.toString('base64')+"" : "", - // }, - // "attributes": - // { "durationInMillis": Math.floor((metadata.format.duration?? 0) * 1000), - // "hasLyrics": false, - // "playParams": { "id": "/ciderlocal?" + audio, "kind": "song", "isLibrary": true, "reporting": false }, - // "trackNumber": 0, - // "discNumber": 0, - // "genreNames": [""], - // "name": metadata.common.title, - // "albumName": metadata.common.album, - // "artistName": metadata.common.artist}} - metadatalist.push(form) - } - } catch (e) { } - } - // console.log('metadatalist', metadatalist); - this.localSongs = metadatalist; + ipcMain.handle("scanLibrary", async (event, folders) => { + const oldmetadatalist = await LocalFiles.sendOldLibrary() + BrowserWindow.win.webContents.send('getUpdatedLocalList', oldmetadatalist); + const metadatalist = await LocalFiles.scanLibrary() BrowserWindow.win.webContents.send('getUpdatedLocalList', metadatalist); - } - - ) + LocalFiles.cleanUpDB() + }) ipcMain.on('writeWAV', (event, leftpcm, rightpcm, bufferlength) => { @@ -1479,10 +1383,17 @@ export class BrowserWindow { } }); + ipcMain.on('open-appdata', (_event) => { shell.openPath(app.getPath('userData')); }); + ipcMain.handle('folderSelector', async (_event) => { + let u = await dialog.showOpenDialog({ + properties: ['openDirectory', 'multiSelections'] + }); + return u.filePaths + }); //#region Cider Connect ipcMain.on('cc-auth', (_event) => { diff --git a/src/main/base/store.ts b/src/main/base/store.ts index 6bf6071c..a014ae45 100644 --- a/src/main/base/store.ts +++ b/src/main/base/store.ts @@ -31,7 +31,8 @@ export class Store { "applemusic": false, "library": false, "amplaylists": false, - "playlists": false + "playlists": false, + "localLibrary": false }, "onStartup": { "enabled": false, @@ -155,6 +156,7 @@ export class Store { "sortOrder": "asc", "viewAs": "covers" }, + "localPaths": [] }, "audio": { "volume": 1, diff --git a/src/main/base/utils.ts b/src/main/base/utils.ts index 46b865be..f2108528 100644 --- a/src/main/base/utils.ts +++ b/src/main/base/utils.ts @@ -67,6 +67,14 @@ export class utils { static getIPCMain(): Electron.IpcMain { return ipcMain } + + /* + * Get the Express instance + * @returns {any} + */ + static getExpress(): any { + return bw.express + } /** * Fetches the i18n locale for the given language. diff --git a/src/main/base/vcomponents.json b/src/main/base/vcomponents.json index 42f86c68..53232dc8 100644 --- a/src/main/base/vcomponents.json +++ b/src/main/base/vcomponents.json @@ -60,6 +60,7 @@ "components/fullscreen", "components/miniplayer", "components/castmenu", + "components/pathmenu", "components/airplay-modal", "components/artist-chip", "components/hello-world", diff --git a/src/main/index.ts b/src/main/index.ts index 4a74f0e9..4c13a1a8 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -14,7 +14,6 @@ import {init as Sentry} from "@sentry/electron"; import {RewriteFrames} from "@sentry/integrations"; import {components, ipcMain} from "electron" - // Analytics for debugging fun yeah. Sentry({ dsn: "https://68c422bfaaf44dea880b86aad5a820d2@o954055.ingest.sentry.io/6112214", diff --git a/src/main/providers/local/db/index.ts b/src/main/providers/local/db/index.ts new file mode 100644 index 00000000..89063d20 --- /dev/null +++ b/src/main/providers/local/db/index.ts @@ -0,0 +1,12 @@ +import * as PouchDB from 'pouchdb-node'; +import {join} from 'path'; +import {app} from "electron"; +PouchDB.plugin(require('pouchdb-upsert')); +export class ProviderDB { + public static db: any = null + static init() { + if (ProviderDB.db == null){ + ProviderDB.db = new PouchDB(join(app.getPath('userData'), 'tracksdb')) + } + } +} \ No newline at end of file diff --git a/src/main/providers/local/index.ts b/src/main/providers/local/index.ts new file mode 100644 index 00000000..332e30bc --- /dev/null +++ b/src/main/providers/local/index.ts @@ -0,0 +1,172 @@ +import { ProviderDB } from "./db"; +import * as path from 'path'; +const { readdir } = require('fs').promises; +import { utils } from '../../base/utils'; +import * as mm from 'music-metadata'; +import {Md5} from 'ts-md5/dist/md5'; +import e from "express"; + +export class LocalFiles { + static localSongs: any = []; + static localSongsArts: any = []; + public static DB = ProviderDB.db; + + static getDataType(item_id : String | any){ + if ((item_id ?? ('')).startsWith('ciderlocalart')) + return 'artwork' + else if ((item_id ?? ('')).startsWith('ciderlocal')) + return 'track' + } + + static async sendOldLibrary() { + ProviderDB.init() + let rows = (await ProviderDB.db.allDocs({include_docs: true, + attachments: true})).rows.map((item: any)=>{return item.doc}) + let tracks = rows.filter((item: any) => {return this.getDataType(item._id) == "track"}) + let arts = rows.filter((item: any) => {return this.getDataType(item._id) == "artwork"}) + this.localSongs = tracks; + this.localSongsArts = arts; + return tracks; + } + + static async scanLibrary() { + ProviderDB.init() + let folders = utils.getStoreValue("libraryPrefs.localPaths") + if (folders == null || folders.length == null || folders.length == 0) folders = [] + console.log('folders', folders) + let files: any[] = [] + for (var folder of folders) { + // get files from the Music folder + files = files.concat(await LocalFiles.getFiles(folder)) + } + + //console.log("cider.files", files2); + let supporttedformats = ["mp3", "aac", "webm", "flac", "m4a", "ogg", "wav", "opus"] + let audiofiles = files.filter(f => supporttedformats.includes(f.substring(f.lastIndexOf('.') + 1))); + // console.log("cider.files2", audiofiles, audiofiles.length); + let metadatalist = [] + let metadatalistart = [] + let numid = 0; + for (var audio of audiofiles) { + try { + const metadata = await mm.parseFile(audio); + let lochash = Md5.hashStr(audio) ?? numid; + if (metadata != null) { + let form = { + "id": "ciderlocal" + lochash, + "_id": "ciderlocal" + lochash, + "type": "podcast-episodes", + "href": audio, + "attributes": { + "artwork": { + "width": 3000, + "height": 3000, + "url": "/ciderlocalart/" + "ciderlocal" + lochash, + }, + "topics": [], + "url": "", + "subscribable": true, + "mediaKind": "audio", + "genreNames": [ + "" + ], + // "playParams": { + // "id": "ciderlocal" + numid, + // "kind": "podcast", + // "isLibrary": true, + // "reporting": false }, + "trackNumber": metadata.common.track?.no ?? 0, + "discNumber": metadata.common.disk?.no ?? 0, + "name": metadata.common.title ?? audio.substring(audio.lastIndexOf('\\') + 1), + "albumName": metadata.common.album, + "artistName": metadata.common.artist, + "copyright": metadata.common.copyright ?? "", + "assetUrl": "file:///" + audio, + "contentAdvisory": "", + "releaseDateTime": `${metadata?.common?.year ?? '2022'}-05-13T00:23:00Z`, + "durationInMillis": Math.floor((metadata.format.duration ?? 0) * 1000), + + "offers": [ + { + "kind": "get", + "type": "STDQ" + } + ], + "contentRating": "clean" + } + }; + let art = { + id: "ciderlocal" + lochash, + _id: "ciderlocalart" + lochash, + url: metadata.common.picture != undefined ? metadata.common.picture[0].data.toString('base64') : "", + } + metadatalistart.push(art) + numid += 1; + ProviderDB.db.putIfNotExists(form) + ProviderDB.db.putIfNotExists(art) + metadatalist.push(form) + } + //delete removed tracks + } catch (e) { } + } + this.localSongs = metadatalist; + this.localSongsArts = metadatalistart; + return metadatalist; + } + + static async cleanUpDB(){ + let folders = utils.getStoreValue("libraryPrefs.localPaths") + let rows = (await ProviderDB.db.allDocs({include_docs: true, + attachments: true})).rows.map((item: any)=>{return item.doc}) + let tracks = rows.filter((item: any) => {return this.getDataType(item._id) == "track" && !folders.some((i: String) => {return item["attributes"]["assetUrl"].startsWith("file:///" + i)})}) + let hashs = tracks.map((i: any) => {return i._id}) + for (let hash of hashs){ + try{ + ProviderDB.db.get(hash).then(function (doc: any) { + return ProviderDB.db.remove(doc); + });} catch(e){} + try{ + ProviderDB.db.get(hash.replace('ciderlocal','ciderlocalart')).then(function (doc: any) { + return ProviderDB.db.remove(doc); + });} catch(e){} + } + } + + static async getFiles(dir: any) { + const dirents = await readdir(dir, { withFileTypes: true }); + const files = await Promise.all(dirents.map((dirent: any) => { + const res = path.resolve(dir, dirent.name); + return dirent.isDirectory() ? this.getFiles(res) : res; + })); + return Array.prototype.concat(...files); + } + + static setupHandlers () { + const app = utils.getExpress() + console.log("Setting up handlers for local files") + app.get("/ciderlocal/:songs", (req: any, res: any) => { + const audio = atob(req.params.songs.replace(/_/g, '/').replace(/-/g, '+')); + //console.log('auss', audio) + let data = { + data: + LocalFiles.localSongs.filter((f: any) => audio.split(',').includes(f.id)) + }; + res.send(data); + }); + + app.get("/ciderlocalart/:songs", (req: any, res: any) => { + const audio = req.params.songs; + // metadata.common.picture[0].data.toString('base64') + + res.setHeader('Cache-Control', 'public, max-age=31536000'); + res.setHeader('Expires', new Date(Date.now() + 31536000).toUTCString()); + res.setHeader('Content-Type', 'image/jpeg'); + + let data = + LocalFiles.localSongsArts.filter((f: any) => f.id == audio); + res.status(200).send(Buffer.from(data[0]?.url, 'base64')); + }); + + return app + } +} \ No newline at end of file diff --git a/src/renderer/audio/audio.js b/src/renderer/audio/audio.js index d4c2da18..6809acb1 100644 --- a/src/renderer/audio/audio.js +++ b/src/renderer/audio/audio.js @@ -386,7 +386,7 @@ const CiderAudio = { if (this._isBufferFull()) { this._flush(); } - let dataLength = audioRawData[0].length; + let dataLength = audioRawData[0]?.length ?? 0; for (let idx=0; idx { - console.log("cider-local", data); + // console.log("cider-local", data); this.library.localsongs = data; }) @@ -1072,6 +1073,8 @@ const app = new Vue({ if (this.cfg.general.themeUpdateNotification && !this.isDev) { this.checkForThemeUpdates() } + + ipcRenderer.invoke("scanLibrary") }, showFoo(querySelector, time) { clearTimeout(this.idleTimer); @@ -1912,7 +1915,7 @@ const app = new Vue({ this.routeView(item.relationships.contents.data[0]) } else if (item.attributes?.link?.url != null) { if (item.attributes.link.url.includes("viewMultiRoom")) { - const params = new Proxy(new URLSearchParams(item.attributes.link.url), { + const params = new Proxy(new URLSearchParams(new URL(item.attributes.link.url).search), { get: (searchParams, prop) => searchParams.get(prop), }); id = params.fcId @@ -3500,7 +3503,15 @@ const app = new Vue({ console.log(truekind, id) try { - if (app.library.songs.displayListing.length > childIndex && parent == "librarysongs") { + if (parent == 'playlist:ciderlocal'){ + let u = app.library.localsongs.map(i => {return i.id}) + app.mk.setQueue({"episodes" : u}).then(()=>{ + let id = app.mk.queue._itemIDs.findIndex(element => element == item.id); + app.mk.changeToMediaAtIndex(id) + }) + + } + else if (app.library.songs.displayListing.length > childIndex && parent == "librarysongs") { console.log(item) if (item && ((app.library.songs.displayListing[childIndex].id != item.id))) { childIndex = app.library.songs.displayListing.indexOf(item) @@ -3804,7 +3815,7 @@ const app = new Vue({ type += "s" } type = type.replace("library-", "") - let id = item.attributes.playParams.catalogId ?? item.attributes.playParams.id ?? item.id + let id = item.attributes.playParams?.catalogId ?? item.attributes.playParams.id ?? item.id let index = types.findIndex(function (type) { return type.type == this diff --git a/src/renderer/style.css b/src/renderer/style.css index e1d1ddff..cbeee3c8 100644 --- a/src/renderer/style.css +++ b/src/renderer/style.css @@ -16458,7 +16458,7 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb { display: flex; justify-content: center; align-items: center; - --chromeHeight1: 60px; + --chromeHeight1: 70px; } .fullscreen-view .app-content-container { width: 100%; @@ -16484,8 +16484,6 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb { left: 0; right: 0; height: var(--chromeHeight1); - background: var(--color1); - backdrop-filter: var(--glassFilter); display: flex; justify-content: center; align-items: center; @@ -16496,8 +16494,9 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb { border: 1px solid #323232; border-radius: 12px; display: flex; - height: 42px; + height: 55px; width: 90%; + backdrop-filter: var(--glassFilter); } .fullscreen-view .fs-header .top-nav-group .app-sidebar-item { background-color: #1e1e1e00; @@ -16510,6 +16509,8 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb { margin: 0px; height: 100%; position: relative; + font-size: 1.1em; + font-weight: 500; } .fullscreen-view .fs-header .top-nav-group .app-sidebar-item:before { --dist: 1px; @@ -16850,10 +16851,25 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb { font-size: 1.1em; font-weight: 380; } +.fullscreen-view .cd-mediaitem-list-item .duration { + font-size: 1.2em; +} .fullscreen-view .cd-mediaitem-list-item .artwork { width: 50px; height: 50px; } +.fullscreen-view .cd-btn-seeall { + font-size: 1.2em; +} +.fullscreen-view h1 { + font-size: 3em; +} +.fullscreen-view h3 { + font-size: 1.5rem; +} +.fullscreen-view .home-page .well.artistfeed-well { + height: 512px; +} .fullscreen-view .header-text { font-size: 3em; height: 3em; @@ -16891,6 +16907,20 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb { gap: 16px; margin-top: 40px; } +.fullscreen-view .playlist-page .playlist-display .playlistInfo .playlist-hero { + transform: unset; +} +.fullscreen-view .artist-page .artist-header { + min-height: 60vh; +} +.fullscreen-view .artist-page .artist-image { + width: 20vh; + height: 20vh; + aspect-ratio: 1; +} +.fullscreen-view .artist-page.animated .artist-header { + min-height: 80vh; +} .fullscreen-view .playlist-page .playlist-body { flex: 1; } diff --git a/src/renderer/views/app/panels.ejs b/src/renderer/views/app/panels.ejs index 2489a2a5..cd5783a1 100644 --- a/src/renderer/views/app/panels.ejs +++ b/src/renderer/views/app/panels.ejs @@ -25,6 +25,9 @@ + + + diff --git a/src/renderer/views/components/fullscreen.ejs b/src/renderer/views/components/fullscreen.ejs index 66babdd8..812e7ded 100644 --- a/src/renderer/views/components/fullscreen.ejs +++ b/src/renderer/views/components/fullscreen.ejs @@ -137,8 +137,8 @@ v-b-tooltip.hover :title="$root.formatVolumeTooltip()"> - - + +
diff --git a/src/renderer/views/components/mediaitem-list-item.ejs b/src/renderer/views/components/mediaitem-list-item.ejs index 307cde37..56b0c409 100644 --- a/src/renderer/views/components/mediaitem-list-item.ejs +++ b/src/renderer/views/components/mediaitem-list-item.ejs @@ -16,7 +16,7 @@ @controller-click="route()" tabindex="0" :class="[{'mediaitem-selected': app.select_hasMediaItem(guid)}, addClasses]"> -
+
@@ -123,8 +123,8 @@ }, mounted() { if (this.item.attributes.playParams) { - this.itemId = this.item.attributes.playParams.id ?? this.item.id; - this.isLibrary = this.item.attributes.playParams.isLibrary ?? false; + this.itemId = this.item.attributes.playParams?.id ?? this.item.id; + this.isLibrary = this.item.attributes.playParams?.isLibrary ?? false; } else { this.itemId = this.item.id; } @@ -143,6 +143,9 @@ return color }, async checkLibrary() { + if (this.item.id.startsWith('ciderlocal')){ + return true + } if (this.addedToLibrary) { return this.addedToLibrary } if (this.item.type.includes("library-playlists") || this.item.type.includes("station")) { this.addedToLibrary = true @@ -155,7 +158,7 @@ }, getClasses() { this.addClasses = {} - if (typeof this.item.attributes.playParams == "undefined") { + if (typeof this.item.attributes.playParams == "undefined" && this.item.type != "podcast-episodes") { this.addClasses["disabled"] = true } if (this.classList) { @@ -167,7 +170,7 @@ }, dragStart(evt) { evt.dataTransfer.setData('text/plain', JSON.stringify({ - type: this.item.attributes.playParams.kind ?? this.item.type, + type: this.item.attributes.playParams?.kind ?? this.item.type, id: this.item.id })) }, @@ -182,20 +185,23 @@ return minutes + ":" + (seconds < 10 ? '0' : '') + seconds; }, getDataType() { + let type = '' if (typeof this.item.attributes.playParams != "undefined") { - if (this.item.attributes.playParams.isLibrary) { - return this.item.type + if (this.item.attributes.playParams?.isLibrary) { + type = this.item.type } else { - return this.item.attributes.playParams.kind + type = this.item.attributes.playParams?.kind } } else { - return this.item.type + type = this.item.type } + if (type == 'podcast-episodes') type = 'episode'; + return type; }, select(e) { let data_type = this.getDataType() - let item_id = this.item.attributes.playParams.id ?? this.item.id - let isLibrary = this.item.attributes.playParams.isLibrary ?? false + let item_id = this.item.attributes.playParams?.id ?? this.item.id + let isLibrary = this.item.attributes.playParams?.isLibrary ?? false if (e.shiftKey) { if (this.index != -1) { @@ -259,8 +265,8 @@ async contextMenu(event) { let self = this let data_type = this.getDataType() - let item_id = this.item.attributes.playParams.id ?? this.item.id - let isLibrary = this.item.attributes.playParams.isLibrary ?? false + let item_id = this.item.attributes.playParams?.id ?? this.item.id + let isLibrary = this.item.attributes.playParams?.isLibrary ?? false let useMenu = "normal" if (app.selectedMediaItems.length <= 1) { @@ -319,6 +325,7 @@ app.mk.playLater({ [kind + "s"]: itemsToPlay[kind] }) } } + console.log(itemsToPlay) app.selectedMediaItems = [] } }, @@ -400,7 +407,9 @@ "name": app.getLz('action.playNext'), "icon": "./assets/arrow-bend-up.svg", "action": function () { - app.mk.playNext({ [self.item.attributes.playParams.kind ?? self.item.type]: self.item.attributes.playParams.id ?? self.item.id }) + let type = self.item.attributes.playParams?.kind ?? self.item.type + if (type == "podcast-episodes") {type = "episode"} + app.mk.playNext({ [type]: self.item.attributes.playParams?.id ?? self.item.id }) app.mk.queue._reindex() app.selectedMediaItems = [] } @@ -409,7 +418,9 @@ "name": app.getLz('action.playLater'), "icon": "./assets/arrow-bend-down.svg", "action": function () { - app.mk.playLater({ [self.item.attributes.playParams.kind ?? self.item.type]: self.item.attributes.playParams.id ?? self.item.id }) + let type = self.item.attributes.playParams?.kind ?? self.item.type + if (type == "podcast-episodes") {type = "episode"} + app.mk.playLater({ [type]: self.item.attributes.playParams?.id ?? self.item.id }) app.mk.queue._reindex() app.selectedMediaItems = [] } @@ -418,7 +429,7 @@ "icon": "./assets/feather/radio.svg", "name": app.getLz('action.startRadio'), "action": function () { - app.mk.setStationQueue({ song: self.item.attributes.playParams.id ?? self.item.id }).then(() => { + app.mk.setStationQueue({ song: self.item.attributes.playParams?.id ?? self.item.id }).then(() => { app.mk.play() app.selectedMediaItems = [] }) @@ -444,7 +455,7 @@ "action": function () { if (!self.item.attributes.url && self.item.relationships) { if (self.item.relationships.catalog) { - app.mkapi(self.item.attributes.playParams.kind, false, self.item.relationships.catalog.data[0].id).then(u => { self.app.copyToClipboard((u.data.data.length && u.data.data.length > 0) ? u.data.data[0].attributes.url : u.data.data.attributes.url) }) + app.mkapi(self.item.attributes.playParams?.kind, false, self.item.relationships.catalog.data[0].id).then(u => { self.app.copyToClipboard((u.data.data.length && u.data.data.length > 0) ? u.data.data[0].attributes.url : u.data.data.attributes.url) }) } } else { self.app.copyToClipboard(self.item.attributes.url) @@ -457,7 +468,7 @@ "action": function () { if (!self.item.attributes.url && self.item.relationships) { if (self.item.relationships.catalog) { - app.mkapi(self.item.attributes.playParams.kind, false, self.item.relationships.catalog.data[0].id).then(u => { self.app.songLinkShare((u.data.data.length && u.data.data.length > 0) ? u.data.data[0].attributes.url : u.data.data.attributes.url) }) + app.mkapi(self.item.attributes.playParams?.kind, false, self.item.relationships.catalog.data[0].id).then(u => { self.app.songLinkShare((u.data.data.length && u.data.data.length > 0) ? u.data.data[0].attributes.url : u.data.data.attributes.url) }) } } else { self.app.songLinkShare(self.item.attributes.url) @@ -526,9 +537,9 @@ }, addToLibrary() { let item = this.item - if (item.attributes.playParams.id) { - console.log('adding to library', item.attributes.playParams.id) - app.addToLibrary(item.attributes.playParams.id.toString()) + if (item.attributes.playParams?.id) { + console.log('adding to library', item.attributes.playParams?.id) + app.addToLibrary(item.attributes.playParams?.id.toString()) this.addedToLibrary = true } else if (item.id) { console.log('adding to library', item.id) @@ -539,14 +550,14 @@ async removeFromLibrary() { let item = this.item let params = { "fields[songs]": "inLibrary", "fields[albums]": "inLibrary", "relate": "library" } - let id = item.id ?? item.attributes.playParams.id - let res = await app.mkapi(item.attributes.playParams.kind ?? item.type, item.attributes.playParams.isLibrary ?? false, item.attributes.playParams.id ?? item.id, params); + let id = item.id ?? item.attributes.playParams?.id + let res = await app.mkapi(item.attributes.playParams?.kind ?? item.type, item.attributes.playParams?.isLibrary ?? false, item.attributes.playParams?.id ?? item.id, params); if (res && res.relationships && res.relationships.library && res.relationships.library.data && res.relationships.library.data.length > 0) { id = res.relationships.library.data[0].id } - let kind = this.item.attributes.playParams.kind ?? this.data.item ?? ''; + let kind = this.item.attributes.playParams?.kind ?? this.data.item ?? ''; let truekind = (!kind.endsWith("s")) ? (kind + "s") : kind; - if (item.attributes.playParams.id) { + if (item.attributes.playParams?.id) { console.log('remove from library', id) app.removeFromLibrary(truekind, id) this.addedToLibrary = false @@ -560,9 +571,9 @@ let item = this.item let parent = this.parent let childIndex = this.index - let kind = (item.attributes.playParams ? (item.attributes.playParams.kind ?? (item.type ?? '')) : (item.type ?? '')); - let id = (item.attributes.playParams ? (item.attributes.playParams.id ?? (item.id ?? '')) : (item.id ?? ''));; - let isLibrary = item.attributes.playParams ? (item.attributes.playParams.isLibrary ?? false) : false; + let kind = (item.attributes.playParams ? (item.attributes.playParams?.kind ?? (item.type ?? '')) : (item.type ?? '')); + let id = (item.attributes.playParams ? (item.attributes.playParams?.id ?? (item.id ?? '')) : (item.id ?? ''));; + let isLibrary = item.attributes.playParams ? (item.attributes.playParams?.isLibrary ?? false) : false; let truekind = (!kind.endsWith("s")) ? (kind + "s") : kind; console.log(item, parent, childIndex, kind, id, isLibrary, kind == "playlists", id.startsWith("p.") || id.startsWith("pl.u")) app.mk.stop().then(() => { @@ -578,7 +589,7 @@ array[j] = temp; } } - app.mk.setQueue({ [truekind]: [item.attributes.playParams.id ?? item.id], parameters: { l: this.app.mklang } }).then(function () { + app.mk.setQueue({ [truekind]: [item.attributes.playParams?.id ?? item.id], parameters: { l: this.app.mklang } }).then(function () { app.mk.play().then(function () { var playlistId = id function getPlaylist(id, isLibrary) { @@ -624,12 +635,12 @@ } else { - app.playMediaItemById(item.attributes.playParams.id ?? item.id, item.attributes.playParams.kind ?? item.type, item.attributes.playParams.isLibrary ?? false, item.attributes.url) + app.playMediaItemById(item.attributes.playParams?.id ?? item.id, item.attributes.playParams?.kind ?? item.type, item.attributes.playParams?.isLibrary ?? false, item.attributes.url) } }) }, route() { - let kind = (this.item.attributes.playParams ? (this.item.attributes.playParams.kind ?? (this.item.type ?? '')) : (this.item.type ?? '')); + let kind = (this.item.attributes.playParams ? (this.item.attributes.playParams?.kind ?? (this.item.type ?? '')) : (this.item.type ?? '')); if (kind.toLowerCase().includes('album') || kind.toLowerCase().includes('playlist')) { app.routeView(this.item) } else { diff --git a/src/renderer/views/components/pathmenu.ejs b/src/renderer/views/components/pathmenu.ejs new file mode 100644 index 00000000..05e6db0a --- /dev/null +++ b/src/renderer/views/components/pathmenu.ejs @@ -0,0 +1,65 @@ + + \ No newline at end of file diff --git a/src/renderer/views/components/settings-window.ejs b/src/renderer/views/components/settings-window.ejs index baac65f4..d46c0720 100644 --- a/src/renderer/views/components/settings-window.ejs +++ b/src/renderer/views/components/settings-window.ejs @@ -228,6 +228,16 @@
+
+
+ {{'Local files path'}} +
+
+ +
+
@@ -1893,8 +1903,11 @@ app.confirm(app.getLz("settings.prompt.general.keybindings.update.success"), (ok) => { if (ok) ipcRenderer.invoke("relaunchApp") }) - } + }, /* keybindings */ + openLocalSongsPathMenu() { + app.modals.pathMenu = true + } } }) \ No newline at end of file diff --git a/src/renderer/views/components/sidebar-playlist.ejs b/src/renderer/views/components/sidebar-playlist.ejs index 31d2a505..1a24eb7b 100644 --- a/src/renderer/views/components/sidebar-playlist.ejs +++ b/src/renderer/views/components/sidebar-playlist.ejs @@ -207,7 +207,44 @@ openPlaylist(item) { this.$root.appRoute(`playlist_` + item.id); this.$root.showingPlaylist = []; - this.$root.getPlaylistFromID(this.$root.page.substring(9), true) + if (item.id == 'ciderlocal') { + this.$root.showingPlaylist = { + "id": "ciderlocal", + "type": "library-playlists", + "href": "", + "attributes": { + "artwork": { + "width": null, + "height": null, + "url": "", + "hasP3": false + }, + "dateAdded": "2021-02-16T03:39:47Z", + "name": "Local Songs", + "canDelete": true, + "hasCatalog": true, + "canEdit": true, + "playParams": { + "id": "ciderlocal", + "kind": "playlist", + "isLibrary": true, + }, + "isPublic": true, + "description": { + "standard": "" + } + }, + "relationships": { + "tracks": { + "href": "", + "data": this.$root.library.localsongs + } + } + } + this.$root.playlists.loadingState = 1; + } else { + this.$root.getPlaylistFromID(this.$root.page.substring(9), true) + } }, getPlaylistChildren(item) { let self = this diff --git a/src/renderer/views/components/sidebar.ejs b/src/renderer/views/components/sidebar.ejs index d642b7c0..cf3a9697 100644 --- a/src/renderer/views/components/sidebar.ejs +++ b/src/renderer/views/components/sidebar.ejs @@ -138,7 +138,16 @@ > - + @@ -207,7 +207,7 @@ @@ -305,17 +305,24 @@ }, mounted: function () { this.$nextTick(function () { - this.isInLibrary() - }) + if (this.data.id != "ciderlocal") { + this.isInLibrary() + } else { + if (this.data.relationships != null && this.data.id == "ciderlocal") { + this.displayListing = this.data.relationships.tracks.data + } + + this.inPlaylist = this.data.type == "library-playlists"; + }}) }, beforeMount() { - if( window.location.hash.includes("playlist") ) { + if (window.location.hash.includes("playlist")) { window.addEventListener('keydown', this.getCopiedPlayListSongs); window.addEventListener('keydown', this.pasteSongs); } }, beforeDestroy() { - if( window.location.hash.includes("playlist") ) { + if (window.location.hash.includes("playlist")) { window.removeEventListener('keydown', this.getCopiedPlayListSongs); window.removeEventListener('keydown', this.pasteSongs); } @@ -326,10 +333,14 @@ this.isInLibrary() this.getBadges() - if (this.data.relationships) { - this.generateNestedPlaylist(this.data.relationships.tracks.data) - if (!this.hasNestedPlaylist) { + if (this.data.relationships != null) { + if (this.data.id == "ciderlocal") { this.displayListing = this.data.relationships.tracks.data + } else { + this.generateNestedPlaylist(this.data.relationships.tracks.data) + if (!this.hasNestedPlaylist) { + this.displayListing = this.data.relationships.tracks.data + } } } @@ -382,21 +393,21 @@ this.headerVisible = visible }, hasHero() { - if(this.data.attributes?.editorialArtwork?.bannerUber){ + if (this.data.attributes?.editorialArtwork?.bannerUber) { return this.data.attributes?.editorialArtwork?.bannerUber.url - } else if(this.data.attributes?.editorialArtwork?.subscriptionHero) { + } else if (this.data.attributes?.editorialArtwork?.subscriptionHero) { return this.data.attributes?.editorialArtwork?.subscriptionHero.url - } else if(this.data.attributes?.editorialArtwork?.storeFlowcase){ + } else if (this.data.attributes?.editorialArtwork?.storeFlowcase) { return this.data.attributes?.editorialArtwork?.storeFlowcase.url } return false; }, hasHeroObject() { - if(this.data.attributes?.editorialArtwork?.bannerUber){ + if (this.data.attributes?.editorialArtwork?.bannerUber) { return this.data.attributes?.editorialArtwork?.bannerUber - } else if(this.data.attributes?.editorialArtwork?.subscriptionHero) { + } else if (this.data.attributes?.editorialArtwork?.subscriptionHero) { return this.data.attributes?.editorialArtwork?.subscriptionHero - } else if(this.data.attributes?.editorialArtwork?.storeFlowcase){ + } else if (this.data.attributes?.editorialArtwork?.storeFlowcase) { return this.data.attributes?.editorialArtwork?.storeFlowcase } return []; @@ -774,14 +785,17 @@ } } - const id = this.data.attributes.playParams.id ?? this.data.id; + const id = this.data.attributes.playParams?.id ?? this.data.id; //console.log("1") - const kind = this.data.attributes.playParams.kind ?? this.data.type ?? ''; + const kind = this.data.attributes.playParams?.kind ?? this.data.type ?? ''; //console.log("1") + if (kind == "podcast-episodes") {kind = "episode"} const truekind = (!kind.endsWith("s")) ? (kind + "s") : kind; let query = (this.data ?? app.showingPlaylist).relationships.tracks.data.map(item => new MusicKit.MediaItem(item)); - app.mk.stop().then(function () { + + app.mk.stop().then(() => { + if (id != 'ciderlocal'){ app.mk.setQueue({ [truekind]: [id], parameters: { l: app.mklang } }).then(function () { app.mk.play().then(function () { if (query.length > 100) { @@ -792,7 +806,12 @@ app.mk.queue.append(u) } }) - }) + })} else { + let u = app.library.localsongs.map(i => {return i.id}) + app.mk.setQueue({"episodes" : u}).then(()=>{ + app.mk.play() + }) + } }) @@ -802,7 +821,7 @@ return "d-none"; }, async getCopiedPlayListSongs(event) { - if( event.ctrlKey && event.keyCode === 67 ) { + if (event.ctrlKey && event.keyCode === 67) { let urls = []; app.selectedMediaItems.forEach(item => { this.app.mk.api.v3.music(`/v1/me/library/songs/${item.id}`).then((response) => { @@ -816,14 +835,14 @@ } }, async pasteSongs(event) { - if( event.ctrlKey && event.keyCode === 86 && this.data.attributes.canEdit ) { + if (event.ctrlKey && event.keyCode === 86 && this.data.attributes.canEdit) { let clipboard = await navigator.clipboard.readText() let songs = [] clipboard = clipboard.split(",") clipboard.forEach(item => { songs.push({ - id: item.substring(item.indexOf("i=")+2, item.length), + id: item.substring(item.indexOf("i=") + 2, item.length), type: "songs", }) }) @@ -901,4 +920,4 @@ } } }) - + \ No newline at end of file diff --git a/src/renderer/views/pages/settings.ejs b/src/renderer/views/pages/settings.ejs index f67799af..0b9fcbe8 100644 --- a/src/renderer/views/pages/settings.ejs +++ b/src/renderer/views/pages/settings.ejs @@ -220,6 +220,16 @@ +
+
+ {{'Local files path'}} +
+
+ +
+
@@ -1147,7 +1157,6 @@ -
{{$root.getLz('settings.option.experimental.reinstallwidevine')}} @@ -1523,6 +1532,9 @@ }, filterChange(e) { this.$root.cfg.connectivity.lastfm.filter_types[e.target.value] = e.target.checked; + }, + openLocalSongsPathMenu() { + app.modals.pathMenu = true } } }) diff --git a/tsconfig.json b/tsconfig.json index f9e5503c..c1b41500 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,7 @@ "paths": { "*": ["node_modules/*"] }, + "skipLibCheck": true, "allowSyntheticDefaultImports": true, "typeRoots": [ "node_modules/musickit-typescript",