Merge branch 'upcoming' into fuck-git

This commit is contained in:
cryptofyre 2022-01-18 21:25:30 -06:00 committed by GitHub
commit 03e771fed1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
64 changed files with 6305 additions and 2036 deletions

1
.gitignore vendored
View file

@ -4,6 +4,7 @@ dist
yarn*
package-lock.json
.yarnclean
build
# Misc
.idea

262
index.js
View file

@ -1,262 +0,0 @@
require('v8-compile-cache');
const { app, components } = require('electron'), { resolve, join } = require("path"),
CiderBase = require('./src/main/cider-base');
const customProtocols = require('./package.json').fileAssociations[0].protocols
console.log(customProtocols)
const comps = components;
// Analytics for debugging.
const ElectronSentry = require("@sentry/electron");
ElectronSentry.init({ dsn: "https://68c422bfaaf44dea880b86aad5a820d2@o954055.ingest.sentry.io/6112214" });
const configDefaults = {
"general": {
"close_behavior": 0, // 0 = close, 1 = minimize, 2 = minimize to tray
"startup_behavior": 0, // 0 = nothing, 1 = open on startup
"discord_rpc": 1, // 0 = disabled, 1 = enabled as Cider, 2 = enabled as Apple Music
"discordClearActivityOnPause": 1, // 0 = disabled, 1 = enabled
"volume": 1
},
"home": {
"followedArtists": [],
"favoriteItems": []
},
"libraryPrefs": {
"songs": {
"sort": "name",
"sortOrder": "asc",
"size": "normal"
}
},
"audio": {
"quality": "990",
"seamless_audio": true,
"normalization": false,
"spatial": false,
"spatial_properties": {
"presets": [],
"gain": 0.8,
"listener_position": [0, 0, 0],
"audio_position": [0, 0, 0],
"room_dimensions": {
"width": 32,
"height": 12,
"depth": 32
},
"room_materials": {
"left": 'metal',
"right": 'metal',
"front": 'brick-bare',
"back": 'brick-bare',
"down": 'acoustic-ceiling-tiles',
"up": 'acoustic-ceiling-tiles',
}
}
},
"visual": {
"theme": "",
"scrollbars": 0, // 0 = show on hover, 2 = always hide, 3 = always show
"refresh_rate": 0,
"animated_artwork": "limited", // 0 = always, 1 = limited, 2 = never
"animated_artwork_qualityLevel": 1,
"bg_artwork_rotation": false,
"hw_acceleration": "default", // default, webgpu, disabled
"videoRes": 720
},
"lyrics": {
"enable_mxm": false,
"mxm_karaoke": false,
"mxm_language": "en",
"enable_yt": false,
},
"lastfm": {
"enabled": false,
"scrobble_after": 50,
"auth_token": "",
"enabledRemoveFeaturingArtists": true,
"NowPlaying": "true"
},
"advanced": {
"AudioContext": false,
"experiments": []
}
}
const merge = (target, source) => {
// Iterate through `source` properties and if an `Object` set property to merge of `target` and `source` properties
for (const key of Object.keys(source)) {
if (source[key] instanceof Object) Object.assign(source[key], merge(target[key], source[key]))
}
// Join `target` and modified `source`
Object.assign(target || {}, source)
return target
}
const Store = require("electron-store");
app.cfg = new Store({
defaults: configDefaults
});
let currentCfg = app.cfg.get()
app.cfg.set(merge(configDefaults, currentCfg))
app.paths = {
ciderCache: resolve(app.getPath("userData"), "CiderCache"),
themes: resolve(app.getPath("userData"), "Themes"),
plugins: resolve(app.getPath("userData"), "Plugins"),
}
switch (app.cfg.get("visual.hw_acceleration")) {
default:
case "default":
app.commandLine.appendSwitch('enable-accelerated-mjpeg-decode')
app.commandLine.appendSwitch('enable-accelerated-video')
app.commandLine.appendSwitch('disable-gpu-driver-bug-workarounds')
app.commandLine.appendSwitch('ignore-gpu-blacklist')
app.commandLine.appendSwitch('enable-native-gpu-memory-buffers')
app.commandLine.appendSwitch('enable-accelerated-video-decode');
app.commandLine.appendSwitch('enable-gpu-rasterization');
app.commandLine.appendSwitch('enable-native-gpu-memory-buffers');
app.commandLine.appendSwitch('enable-oop-rasterization');
break;
case "webgpu":
console.info("WebGPU is enabled.");
app.commandLine.appendSwitch('enable-unsafe-webgpu')
break;
case "disabled":
console.info("Hardware acceleration is disabled.");
app.commandLine.appendSwitch('disable-gpu')
break;
}
// Creating the Application Window and Calling all the Functions
function CreateWindow() {
if (app.isQuiting) {
app.quit();
return;
}
/** CIDER **/
const ciderwin = require("./src/main/cider-base")
app.win = ciderwin
app.win.Start()
/** CIDER **/
}
if (process.platform === "linux") {
app.commandLine.appendSwitch('disable-features', 'MediaSessionService');
}
app.commandLine.appendSwitch('no-sandbox');
// app.commandLine.appendSwitch('js-flags', '--max-old-space-size=1024')
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* App Event Handlers
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
app.whenReady().then(async() => {
if (process.platform === "win32") {
app.commandLine.appendSwitch('high-dpi-support', 'true')
app.commandLine.appendSwitch('force-device-scale-factor', '1')
app.commandLine.appendSwitch('disable-pinch');
}
if (comps == null) {
app.on("widevine-ready", () => {
console.log('[Cider] Application is Ready. Creating Window.')
if (!app.isPackaged) {
console.info('[Cider] Running in development mode.')
require('vue-devtools').install()
}
CreateWindow()
})
return
}
await comps.whenReady();
console.log('components ready:', comps.status());
console.log('[Cider] Application is Ready. Creating Window.')
if (!app.isPackaged) {
console.info('[Cider] Running in development mode.')
require('vue-devtools').install()
}
CreateWindow()
})
app.on('before-quit', () => {
console.warn(`${app.getName()} exited.`);
});
// Widevine Stuff
app.on('widevine-ready', (version, lastVersion) => {
if (null !== lastVersion) {
console.log('[Cider][Widevine] Widevine ' + version + ', upgraded from ' + lastVersion + ', is ready to be used!')
} else {
console.log('[Cider][Widevine] Widevine ' + version + ' is ready to be used!')
}
})
app.on('widevine-update-pending', (currentVersion, pendingVersion) => {
console.log('[Cider][Widevine] Widevine ' + currentVersion + ' is ready to be upgraded to ' + pendingVersion + '!')
})
app.on('widevine-error', (error) => {
console.log('[Cider][Widevine] Widevine installation encountered an error: ' + error)
app.exit()
})
if (process.defaultApp) {
if (process.argv.length >= 2) {
customProtocols.forEach((customProtocol) => {
app.setAsDefaultProtocolClient(customProtocol, process.execPath, [resolve(process.argv[1])])
})
}
} else {
/*
* cider - Custom Cider Protocol
* ame - Custom AME Protocol (Backwards Compat.)
* itms - iTunes HTTP Protocol
* itmss - iTunes HTTPS Protocol
* musics - macOS Client Protocol
* music - macOS Client Protocol
*/
customProtocols.forEach((customProtocol) => {
app.setAsDefaultProtocolClient(customProtocol)
})
}
app.on('open-url', (event, url) => {
event.preventDefault()
if (customProtocols.some(protocol => url.includes(protocol))) {
CiderBase.LinkHandler(url)
}
})
app.on('second-instance', (_e, argv) => {
console.warn(`[InstanceHandler][SecondInstanceHandler] Second Instance Started with args: [${argv.join(', ')}]`)
// Checks if first instance is authorized and if second instance has protocol args
argv.forEach((value) => {
if (customProtocols.some(protocol => value.includes(protocol))) {
CiderBase.LinkHandler(value);
}
})
if (argv.includes("--force-quit")) {
console.warn('[InstanceHandler][SecondInstanceHandler] Force Quit found. Quitting App.');
// app.isQuiting = true
app.quit()
} else if (CiderBase.win && true) { // If a Second Instance has Been Started
console.warn('[InstanceHandler][SecondInstanceHandler] Showing window.');
app.win.show()
app.win.focus()
}
})
if (!app.requestSingleInstanceLock() && true) {
console.warn("[InstanceHandler] Existing Instance is Blocking Second Instance.");
app.quit();
// app.isQuiting = true
}

View file

@ -5,7 +5,8 @@
"version": "1.0.0",
"description": "A new look into listening and enjoying music in style and performance.",
"license": "MIT",
"author": "Cider Collective <cryptofyre@cryptofyre.org> (https://cider.sh)",
"main": "./build/index.js",
"author": "Cider Collective <cryptofyre@cider.sh> (https://cider.sh)",
"repository": "https://github.com/ciderapp/Cider.git",
"bugs": {
"url": "https://github.com/ciderapp/Cider/issues?q=is%3Aopen+is%3Aissue+label%3Abug"
@ -13,10 +14,14 @@
"homepage": "https://cider.sh/",
"buildResources": "resources",
"scripts": {
"init": "yarn install --force",
"start": "electron . --enable-accelerated-mjpeg-decode --enable-accelerated-video --disable-gpu-driver-bug-workarounds --ignore-gpu-blacklist --enable-native-gpu-memory-buffers",
"build": "tsc",
"watch": "tsc --watch",
"start": "run-script-os",
"start:win32": "npm run build && set ELECTRON_ENABLE_LOGGING=true && electron ./build/index.js --enable-accelerated-mjpeg-decode --enable-accelerated-video --disable-gpu-driver-bug-workarounds --ignore-gpu-blacklist --enable-native-gpu-memory-buffers",
"start:linux": "npm run build && export ELECTRON_ENABLE_LOGGING=true && electron ./build/index.js --enable-accelerated-mjpeg-decode --enable-accelerated-video --disable-gpu-driver-bug-workarounds --ignore-gpu-blacklist --enable-native-gpu-memory-buffers",
"start:darwin": "npm run build && export ELECTRON_ENABLE_LOGGING=true && electron ./build/index.js --enable-accelerated-mjpeg-decode --enable-accelerated-video --disable-gpu-driver-bug-workarounds --ignore-gpu-blacklist --enable-native-gpu-memory-buffers",
"pack": "electron-builder --dir",
"dist": "electron-builder",
"dist": "npm run build && electron-builder",
"msft": "electron-builder -c msft-package.json",
"postinstall": "electron-builder install-app-deps"
},
@ -25,7 +30,7 @@
"discord-rpc": "^4.0.1",
"ejs": "^3.1.6",
"electron-fetch": "^1.7.4",
"electron-log": "^4.4.3",
"electron-log": "^4.4.4",
"electron-store": "^8.0.1",
"electron-updater": "^4.6.1",
"electron-window-state": "^5.0.3",
@ -37,17 +42,20 @@
"qrcode-terminal": "^0.12.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"run-script-os": "^1.1.6",
"source-map-support": "^0.5.21",
"v8-compile-cache": "^2.3.0",
"ws": "^8.3.0",
"ws": "^8.4.2",
"xml2js": "^0.4.23",
"youtube-search-without-api-key": "^1.0.7"
},
"devDependencies": {
"@types/express": "^4.17.13",
"electron": "https://github.com/castlabs/electron-releases.git",
"electron-builder": "^22.14.5",
"electron-webpack": "^2.8.2",
"musickit-typescript": "^1.2.4",
"typescript": "^4.5.4",
"vue-devtools": "^5.1.4",
"webpack": "~5.65.0"
},
@ -74,9 +82,9 @@
}
],
"build": {
"electronVersion": "16.0.6",
"electronVersion": "16.0.7",
"electronDownload": {
"version": "16.0.6+wvcus",
"version": "16.0.7+wvcus",
"mirror": "https://github.com/castlabs/electron-releases/releases/download/v"
},
"appId": "cider",
@ -95,9 +103,9 @@
],
"extends": null,
"files": [
"**/*",
"./src/**/*",
"./resources/icons/icon.*"
"./build/**/*",
"./resources/icons/icon.*",
"./src/**/*"
],
"linux": {
"target": [
@ -126,7 +134,8 @@
"icon": "resources/icons/icon.ico"
},
"directories": {
"buildResources": "."
"buildResources": ".",
"output": "dist"
},
"mac": {
"icon": "./resources/icons/icon.icns",

142
src/main/base/app.ts Normal file
View file

@ -0,0 +1,142 @@
import * as electron from 'electron';
import * as path from 'path';
export class AppEvents {
private static protocols: any = [
"ame",
"cider",
"itms",
"itmss",
"musics",
"music"
]
private static store: any = null;
constructor(store: any) {
console.log('App started');
AppEvents.store = store
AppEvents.start(store);
}
/**
* Handles all actions that occur for the app on start (Mainly commandline arguments)
* @returns {void}
*/
private static start(store: any): void {
console.log('App started');
/**********************************************************************************************************************
* Startup arguments handling
**********************************************************************************************************************/
if (electron.app.commandLine.hasSwitch('version') || electron.app.commandLine.hasSwitch('v')) {
console.log(electron.app.getVersion())
electron.app.exit()
}
// Verbose Check
if (electron.app.commandLine.hasSwitch('verbose')) {
console.log("[Apple-Music-Electron] User has launched the application with --verbose");
}
// Log File Location
if (electron.app.commandLine.hasSwitch('log') || electron.app.commandLine.hasSwitch('l')) {
console.log(path.join(electron.app.getPath('userData'), 'logs'))
electron.app.exit()
}
/***********************************************************************************************************************
* Commandline arguments
**********************************************************************************************************************/
switch (store.get("visual.hw_acceleration")) {
default:
case "default":
electron.app.commandLine.appendSwitch('enable-accelerated-mjpeg-decode')
electron.app.commandLine.appendSwitch('enable-accelerated-video')
electron.app.commandLine.appendSwitch('disable-gpu-driver-bug-workarounds')
electron.app.commandLine.appendSwitch('ignore-gpu-blacklist')
electron.app.commandLine.appendSwitch('enable-native-gpu-memory-buffers')
electron.app.commandLine.appendSwitch('enable-accelerated-video-decode');
electron.app.commandLine.appendSwitch('enable-gpu-rasterization');
electron.app.commandLine.appendSwitch('enable-native-gpu-memory-buffers');
electron.app.commandLine.appendSwitch('enable-oop-rasterization');
break;
case "webgpu":
console.info("WebGPU is enabled.");
electron.app.commandLine.appendSwitch('enable-unsafe-webgpu')
break;
case "disabled":
console.info("Hardware acceleration is disabled.");
electron.app.commandLine.appendSwitch('disable-gpu')
break;
}
/***********************************************************************************************************************
* Protocols
**********************************************************************************************************************/
if (process.defaultApp) {
if (process.argv.length >= 2) {
this.protocols.forEach((protocol: string) => {
electron.app.setAsDefaultProtocolClient(protocol, process.execPath, [path.resolve(process.argv[1])])
})
}
} else {
this.protocols.forEach((protocol: string) => {
electron.app.setAsDefaultProtocolClient(protocol)
})
}
electron.app.on('open-url', (event, url) => {
event.preventDefault()
if (this.protocols.some((protocol: string) => url.includes(protocol))) {
AppEvents.LinkHandler(url)
}
})
}
public quit() {
console.log('App stopped');
}
public ready() {
console.log('App ready');
}
/***********************************************************************************************************************
* Private methods
**********************************************************************************************************************/
private static LinkHandler(arg: string) {
if (!arg) return;
// LastFM Auth URL
if (arg.includes('auth')) {
let authURI = String(arg).split('/auth/')[1]
if (authURI.startsWith('lastfm')) { // If we wanted more auth options
const authKey = authURI.split('lastfm?token=')[1];
AppEvents.store.set('lastfm.enabled', true);
AppEvents.store.set('lastfm.auth_token', authKey);
// AppEvents.window.webContents.send('LastfmAuthenticated', authKey);
// lastfm.authenticate()
}
}
// Play
else if (arg.includes('/play/')) { //Steer away from protocol:// specific conditionals
const playParam = arg.split('/play/')[1]
if (playParam.includes('s/')) { // setQueue can be done with album, song, url, playlist id
console.log(playParam)
let song = playParam.split('s/')[1]
console.warn(`[LinkHandler] Attempting to load song by id: ${song}`);
// AppEvents.window.webContents.executeJavaScript(`
// MusicKit.getInstance().setQueue({ song: '${song}'}).then(function(queue) {
// MusicKit.getInstance().play();
// });
// `)
}
}
}
}

57
src/main/base/plugins.ts Normal file
View file

@ -0,0 +1,57 @@
import * as fs from 'fs';
import * as path from 'path';
import * as electron from 'electron'
export default class PluginHandler {
private basePluginsPath = path.join(__dirname, '../plugins');
private userPluginsPath = path.join(electron.app.getPath('userData'), 'plugins');
private pluginsList: any = {};
constructor() {
this.pluginsList = this.getPlugins();
}
public getPlugins(): any {
let plugins: any = {};
if (fs.existsSync(this.basePluginsPath)) {
fs.readdirSync(this.basePluginsPath).forEach(file => {
if (file.endsWith('.ts') || file.endsWith('.js')) {
const plugin = require(path.join(this.basePluginsPath, file)).default;
if (plugins[file] || plugin.name in plugins) {
console.log(`[${plugin.name}] Plugin already loaded / Duplicate Class Name`);
} else {
plugins[file] = new plugin(electron.app);
}
}
});
}
if (fs.existsSync(this.userPluginsPath)) {
fs.readdirSync(this.userPluginsPath).forEach(file => {
if (file.endsWith('.ts') || file.endsWith('.js')) {
const plugin = require(path.join(this.userPluginsPath, file)).default;
if (plugins[file] || plugin in plugins) {
console.log(`[${plugin.name}] Plugin already loaded / Duplicate Class Name`);
} else {
plugins[file] = new plugin(electron.app);
}
}
});
}
console.log('loaded plugins:', JSON.stringify(plugins))
return plugins;
}
public callPlugins(event: string, ...args: any[]) {
for (const plugin in this.pluginsList) {
if (this.pluginsList[plugin][event]) {
this.pluginsList[plugin][event](...args);
}
}
}
}

130
src/main/base/store.ts Normal file
View file

@ -0,0 +1,130 @@
import * as Store from 'electron-store';
import * as electron from "electron";
export class ConfigStore {
public store: Store | undefined;
private defaults: any = {
"general": {
"close_behavior": 0, // 0 = close, 1 = minimize, 2 = minimize to tray
"startup_behavior": 0, // 0 = nothing, 1 = open on startup
"discord_rpc": 1, // 0 = disabled, 1 = enabled as Cider, 2 = enabled as Apple Music
"discordClearActivityOnPause": 1 // 0 = disabled, 1 = enabled
},
"home": {
"followedArtists": [],
"favoriteItems": []
},
"libraryPrefs": {
"songs": {
"sort": "name",
"sortOrder": "asc",
"size": "normal"
}
},
"audio": {
"volume": 1,
"quality": "990",
"seamless_audio": true,
"normalization": false,
"spatial": false,
"spatial_properties": {
"presets": [],
"gain": 0.8,
"listener_position": [0, 0, 0],
"audio_position": [0, 0, 0],
"room_dimensions": {
"width": 32,
"height": 12,
"depth": 32
},
"room_materials": {
"left": 'metal',
"right": 'metal',
"front": 'brick-bare',
"back": 'brick-bare',
"down": 'acoustic-ceiling-tiles',
"up": 'acoustic-ceiling-tiles',
}
}
},
"visual": {
"theme": "",
"scrollbars": 0, // 0 = show on hover, 2 = always hide, 3 = always show
"refresh_rate": 0,
"animated_artwork": "limited", // 0 = always, 1 = limited, 2 = never
"animated_artwork_qualityLevel": 1,
"bg_artwork_rotation": false,
"hw_acceleration": "default" // default, webgpu, disabled
},
"lyrics": {
"enable_mxm": false,
"mxm_karaoke": false,
"mxm_language": "en",
"enable_yt": false,
},
"lastfm": {
"enabled": false,
"scrobble_after": 30,
"auth_token": "",
"enabledRemoveFeaturingArtists": true,
"NowPlaying": "true"
},
"advanced": {
"AudioContext": false,
"experiments": []
}
}
private migrations: any = {}
constructor() {
this.store = new Store({
name: 'cider-config',
defaults: this.defaults,
migrations: this.migrations,
});
this.store.set(this.mergeStore(this.defaults, this.store.store))
this.ipcHandler(this.store);
}
/**
* Merge Configurations
* @param target The target configuration
* @param source The source configuration
*/
private mergeStore = (target: { [x: string]: any; }, source: { [x: string]: any; }) => {
// Iterate through `source` properties and if an `Object` set property to merge of `target` and `source` properties
for (const key of Object.keys(source)) {
if (key.includes('migrations')) {
continue;
}
if (source[key] instanceof Object) Object.assign(source[key], this.mergeStore(target[key], source[key]))
}
// Join `target` and modified `source`
Object.assign(target || {}, source)
return target
}
/**
* IPC Handler
*/
private ipcHandler(cfg: Store | any): void {
electron.ipcMain.handle('getStoreValue', (event, key, defaultValue) => {
return (defaultValue ? cfg.get(key, true) : cfg.get(key));
});
electron.ipcMain.handle('setStoreValue', (event, key, value) => {
cfg.set(key, value);
});
electron.ipcMain.on('getStore', (event) => {
event.returnValue = cfg.store
})
electron.ipcMain.on('setStore', (event, store) => {
cfg.store = store
})
}
}

443
src/main/base/win.ts Normal file
View file

@ -0,0 +1,443 @@
// @ts-nocheck
import * as path from "path";
import * as electron from "electron";
import * as windowStateKeeper from "electron-window-state";
import * as express from "express";
import * as getPort from "get-port";
import * as yt from "youtube-search-without-api-key";
import * as fs from "fs";
import { Stream } from "stream";
import * as qrcode from "qrcode-terminal";
import * as os from "os";
import {wsapi} from "./wsapi";
export class Win {
win: any | undefined = null;
app: any | undefined = null;
store: any | undefined = null;
devMode: boolean = !electron.app.isPackaged;
constructor(app: electron.App, store: any) {
this.app = app;
this.store = store;
}
private paths: any = {
srcPath: path.join(__dirname, "../../src"),
resourcePath: path.join(__dirname, "../../resources"),
ciderCache: path.resolve(electron.app.getPath("userData"), "CiderCache"),
themes: path.resolve(electron.app.getPath("userData"), "Themes"),
plugins: path.resolve(electron.app.getPath("userData"), "Plugins"),
};
private audioStream: any = new Stream.PassThrough();
private clientPort: number = 0;
private remotePort: number = 6942;
private EnvironmentVariables: object = {
env: {
platform: process.platform,
dev: electron.app.isPackaged,
},
};
private options: any = {
icon: path.join(
this.paths.resourcePath,
`icons/icon.` + (process.platform === "win32" ? "ico" : "png")
),
width: 1024,
height: 600,
x: undefined,
y: undefined,
minWidth: 844,
minHeight: 410,
frame: false,
title: "Cider",
vibrancy: "dark",
transparent: process.platform === "darwin",
hasShadow: false,
webPreferences: {
nodeIntegration: true,
sandbox: true,
allowRunningInsecureContent: true,
contextIsolation: false,
webviewTag: true,
plugins: true,
nodeIntegrationInWorker: false,
webSecurity: false,
preload: path.join(this.paths.srcPath, "./preload/cider-preload.js"),
},
};
/**
* Creates the browser window
*/
async createWindow(): Promise<void> {
this.clientPort = await getPort({ port: 9000 });
this.verifyFiles();
// Load the previous state with fallback to defaults
const windowState = windowStateKeeper({
defaultWidth: 1024,
defaultHeight: 600,
});
this.options.width = windowState.width;
this.options.height = windowState.height;
// Start the webserver for the browser window to load
const ws = new wsapi()
ws.InitWebSockets()
this.startWebServer();
this.win = new electron.BrowserWindow(this.options);
// and load the renderer.
this.startSession();
this.startHandlers();
// Register listeners on Window to track size and position of the Window.
windowState.manage(this.win);
return this.win;
}
/**
* Verifies the files for the renderer to use (Cache, library info, etc.)
*/
private verifyFiles(): void {
const expectedDirectories = ["CiderCache"];
const expectedFiles = [
"library-songs.json",
"library-artists.json",
"library-albums.json",
"library-playlists.json",
"library-recentlyAdded.json",
];
for (let i = 0; i < expectedDirectories.length; i++) {
if (
!fs.existsSync(
path.join(electron.app.getPath("userData"), expectedDirectories[i])
)
) {
fs.mkdirSync(
path.join(electron.app.getPath("userData"), expectedDirectories[i])
);
}
}
for (let i = 0; i < expectedFiles.length; i++) {
const file = path.join(this.paths.ciderCache, expectedFiles[i]);
if (!fs.existsSync(file)) {
fs.writeFileSync(file, JSON.stringify([]));
}
}
}
/**
* Starts the webserver for the renderer process.
*/
private startWebServer(): void {
const app = express();
app.use(express.static(path.join(this.paths.srcPath, "./renderer/")));
app.set("views", path.join(this.paths.srcPath, "./renderer/views"));
app.set("view engine", "ejs");
let firstRequest = true;
app.use((req, res, next) => {
// @ts-ignore
if (
req.url.includes("audio.webm") ||
(req.headers.host.includes("localhost") &&
(this.devMode || req.headers["user-agent"].includes("Electron")))
) {
next();
} else {
res.redirect("https://discord.gg/applemusic");
}
});
app.get("/", (req, res) => {
res.render("main", this.EnvironmentVariables);
});
app.get("/audio.webm", (req, res) => {
try {
req.socket.setTimeout(Number.MAX_SAFE_INTEGER);
// CiderBase.requests.push({req: req, res: res});
// var pos = CiderBase.requests.length - 1;
// req.on("close", () => {
// console.info("CLOSED", CiderBase.requests.length);
// requests.splice(pos, 1);
// console.info("CLOSED", CiderBase.requests.length);
// });
this.audioStream.on("data", (data: any) => {
try {
res.write(data);
} catch (ex) {
console.log(ex);
}
});
} catch (ex) {
console.log(ex);
}
});
//app.use(express.static())
app.listen(this.clientPort, () => {
console.log(`Cider client port: ${this.clientPort}`);
});
/*
* Remote Client (I had no idea how to add it to our existing express server, so I just made another one) -@quacksire
* TODO: Broadcast the remote so that /web-remote/ can connect
* https://github.com/ciderapp/Apple-Music-Electron/blob/818ed18940ff600d76eb59d22016723a75885cd5/resources/functions/handler.js#L1173
*/
const remote = express();
remote.use(express.static(path.join(this.paths.srcPath, "./web-remote/")))
remote.listen(this.remotePort, () => {
console.log(`Cider remote port: ${this.remotePort}`);
if (firstRequest) {
console.log("---- Ignore Me ;) ---");
qrcode.generate(`http://${os.hostname}:${this.remotePort}`);
console.log("---- Ignore Me ;) ---");
/*
*
* USING https://www.npmjs.com/package/qrcode-terminal for terminal
* WE SHOULD USE https://www.npmjs.com/package/qrcode for the remote (or others) for showing to user via an in-app dialog
* -@quacksire
*/
}
firstRequest = false;
})
}
/**
* Starts the session for the renderer process.
*/
private startSession(): void {
// intercept "https://js-cdn.music.apple.com/hls.js/2.141.0/hls.js/hls.js" and redirect to local file "./apple-hls.js" instead
this.win.webContents.session.webRequest.onBeforeRequest(
{
urls: ["https://*/*.js"],
},
(
details: { url: string | string[] },
callback: (arg0: { redirectURL?: string; cancel?: boolean }) => void
) => {
if (details.url.includes("hls.js")) {
callback({
redirectURL: `http://localhost:${this.clientPort}/apple-hls.js`,
});
} else {
callback({
cancel: false,
});
}
}
);
this.win.webContents.session.webRequest.onBeforeSendHeaders(
async (
details: { url: string; requestHeaders: { [x: string]: string } },
callback: (arg0: { requestHeaders: any }) => void
) => {
if (details.url === "https://buy.itunes.apple.com/account/web/info") {
details.requestHeaders["sec-fetch-site"] = "same-site";
details.requestHeaders["DNT"] = "1";
let itspod = await this.win.webContents.executeJavaScript(
`window.localStorage.getItem("music.ampwebplay.itspod")`
);
if (itspod != null)
details.requestHeaders["Cookie"] = `itspod=${itspod}`;
}
callback({ requestHeaders: details.requestHeaders });
}
);
let location = `http://localhost:${this.clientPort}/`;
if (electron.app.isPackaged) {
this.win.loadURL(location);
} else {
this.win.loadURL(location, {
userAgent: "Cider Development Environment",
});
}
}
/**
* Initializes the window handlers
*/
private startHandlers(): void {
/**********************************************************************************************************************
* ipcMain Events
****************************************************************************************************************** */
electron.ipcMain.on("cider-platform", (event) => {
event.returnValue = process.platform;
});
electron.ipcMain.on("get-gpu-mode", (event) => {
event.returnValue = process.platform;
});
electron.ipcMain.on("is-dev", (event) => {
event.returnValue = this.devMode;
});
electron.ipcMain.on("close", () => {
// listen for close event
this.win.close();
});
electron.ipcMain.on("put-library-songs", (event, arg) => {
fs.writeFileSync(
path.join(this.paths.ciderCache, "library-songs.json"),
JSON.stringify(arg)
);
});
electron.ipcMain.on("put-library-artists", (event, arg) => {
fs.writeFileSync(
path.join(this.paths.ciderCache, "library-artists.json"),
JSON.stringify(arg)
);
});
electron.ipcMain.on("put-library-albums", (event, arg) => {
fs.writeFileSync(
path.join(this.paths.ciderCache, "library-albums.json"),
JSON.stringify(arg)
);
});
electron.ipcMain.on("put-library-playlists", (event, arg) => {
fs.writeFileSync(
path.join(this.paths.ciderCache, "library-playlists.json"),
JSON.stringify(arg)
);
});
electron.ipcMain.on("put-library-recentlyAdded", (event, arg) => {
fs.writeFileSync(
path.join(this.paths.ciderCache, "library-recentlyAdded.json"),
JSON.stringify(arg)
);
});
electron.ipcMain.on("get-library-songs", (event) => {
let librarySongs = fs.readFileSync(
path.join(this.paths.ciderCache, "library-songs.json"),
"utf8"
);
event.returnValue = JSON.parse(librarySongs);
});
electron.ipcMain.on("get-library-artists", (event) => {
let libraryArtists = fs.readFileSync(
path.join(this.paths.ciderCache, "library-artists.json"),
"utf8"
);
event.returnValue = JSON.parse(libraryArtists);
});
electron.ipcMain.on("get-library-albums", (event) => {
let libraryAlbums = fs.readFileSync(
path.join(this.paths.ciderCache, "library-albums.json"),
"utf8"
);
event.returnValue = JSON.parse(libraryAlbums);
});
electron.ipcMain.on("get-library-playlists", (event) => {
let libraryPlaylists = fs.readFileSync(
path.join(this.paths.ciderCache, "library-playlists.json"),
"utf8"
);
event.returnValue = JSON.parse(libraryPlaylists);
});
electron.ipcMain.on("get-library-recentlyAdded", (event) => {
let libraryRecentlyAdded = fs.readFileSync(
path.join(this.paths.ciderCache, "library-recentlyAdded.json"),
"utf8"
);
event.returnValue = JSON.parse(libraryRecentlyAdded);
});
electron.ipcMain.handle("getYTLyrics", async (event, track, artist) => {
const u = track + " " + artist + " official video";
return await yt.search(u);
});
electron.ipcMain.handle("setVibrancy", (event, key, value) => {
this.win.setVibrancy(value);
});
electron.ipcMain.on("maximize", () => {
// listen for maximize event
if (this.win.isMaximized()) {
this.win.unmaximize();
} else {
this.win.maximize();
}
});
electron.ipcMain.on("minimize", () => {
// listen for minimize event
this.win.minimize();
});
// Set scale
electron.ipcMain.on("setScreenScale", (event, scale) => {
this.win.webContents.setZoomFactor(parseFloat(scale));
});
/* *********************************************************************************************
* Window Events
* **********************************************************************************************/
if (process.platform === "win32") {
let WND_STATE = {
MINIMIZED: 0,
NORMAL: 1,
MAXIMIZED: 2,
FULL_SCREEN: 3,
};
let wndState = WND_STATE.NORMAL;
this.win.on("resize", (_: any) => {
const isMaximized = this.win.isMaximized();
const isMinimized = this.win.isMinimized();
const isFullScreen = this.win.isFullScreen();
const state = wndState;
if (isMinimized && state !== WND_STATE.MINIMIZED) {
wndState = WND_STATE.MINIMIZED;
} else if (isFullScreen && state !== WND_STATE.FULL_SCREEN) {
wndState = WND_STATE.FULL_SCREEN;
} else if (isMaximized && state !== WND_STATE.MAXIMIZED) {
wndState = WND_STATE.MAXIMIZED;
this.win.webContents.executeJavaScript(`app.chrome.maximized = true`);
} else if (state !== WND_STATE.NORMAL) {
wndState = WND_STATE.NORMAL;
this.win.webContents.executeJavaScript(
`app.chrome.maximized = false`
);
}
});
}
this.win.on("closed", () => {
this.win = null;
});
// Set window Handler
this.win.webContents.setWindowOpenHandler((x: any) => {
if (x.url.includes("apple") || x.url.includes("localhost")) {
return { action: "allow" };
}
electron.shell.openExternal(x.url).catch(console.error);
return { action: "deny" };
});
}
}

284
src/main/base/wsapi.ts Normal file
View file

@ -0,0 +1,284 @@
// @ts-nocheck
import * as ws from "ws";
import * as http from "http";
import * as https from "https";
import * as url from "url";
import * as fs from "fs";
import * as path from "path";
import * as electron from "electron";
const WebSocket = ws;
const WebSocketServer = ws.Server;
private class standardResponse {
status: number;
message: string;
data: any;
type: string;
}
export class wsapi {
port: any = 26369
wss: any = null
clients: []
createId() {
// create random guid
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0,
v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
public async InitWebSockets () {
electron.ipcMain.on('wsapi-updatePlaybackState', (event, arg) => {
wsapi.updatePlaybackState(arg);
})
electron.ipcMain.on('wsapi-returnQueue', (event, arg) => {
wsapi.returnQueue(JSON.parse(arg));
});
electron.ipcMain.on('wsapi-returnSearch', (event, arg) => {
console.log("SEARCH")
wsapi.returnSearch(JSON.parse(arg));
});
electron.ipcMain.on('wsapi-returnSearchLibrary', (event, arg) => {
wsapi.returnSearchLibrary(JSON.parse(arg));
});
electron.ipcMain.on('wsapi-returnDynamic', (event, arg, type) => {
wsapi.returnDynamic(JSON.parse(arg), type);
});
electron.ipcMain.on('wsapi-returnMusicKitApi', (event, arg, method) => {
wsapi.returnMusicKitApi(JSON.parse(arg), method);
});
electron.ipcMain.on('wsapi-returnLyrics', (event, arg) => {
wsapi.returnLyrics(JSON.parse(arg));
});
this.wss = new WebSocketServer({
port: this.port,
perMessageDeflate: {
zlibDeflateOptions: {
// See zlib defaults.
chunkSize: 1024,
memLevel: 7,
level: 3
},
zlibInflateOptions: {
chunkSize: 10 * 1024
},
// Other options settable:
clientNoContextTakeover: true, // Defaults to negotiated value.
serverNoContextTakeover: true, // Defaults to negotiated value.
serverMaxWindowBits: 10, // Defaults to negotiated value.
// Below options specified as default values.
concurrencyLimit: 10, // Limits zlib concurrency for perf.
threshold: 1024 // Size (in bytes) below which messages
// should not be compressed if context takeover is disabled.
}
})
console.log(`WebSocketServer started on port: ${this.port}`);
const defaultResponse = new standardResponse(0, {}, "OK");
this.wss.on('connection', function connection(ws) {
ws.id = wsapi.createId();
console.log(`Client ${ws.id} connected`)
wsapi.clients.push(ws);
ws.on('message', function incoming(message) {
});
// ws on message
ws.on('message', function incoming(message) {
let data = JSON.parse(message);
let response = new standardResponse(0, {}, "OK");;
if (data.action) {
data.action.toLowerCase();
}
switch (data.action) {
default:
response.message = "Action not found";
break;
case "identify":
response.message = "Thanks for identifying!"
response.data = {
id: ws.id
}
ws.identity = {
name: data.name,
author: data.author,
description: data.description,
version: data.version
}
break;
case "play-next":
electron.app.win.webContents.executeJavaScript(`wsapi.playNext(\`${data.type}\`,\`${data.id}\`)`);
response.message = "Play Next";
break;
case "play-later":
electron.app.win.webContents.executeJavaScript(`wsapi.playLater(\`${data.type}\`,\`${data.id}\`)`);
response.message = "Play Later";
break;
case "quick-play":
electron.app.win.webContents.executeJavaScript(`wsapi.quickPlay(\`${data.term}\`)`);
response.message = "Quick Play";
break;
case "get-lyrics":
electron.app.win.webContents.executeJavaScript(`wsapi.getLyrics()`);
break;
case "shuffle":
electron.app.win.webContents.executeJavaScript(`wsapi.toggleShuffle()`);
break;
case "set-shuffle":
if(data.shuffle == true) {
electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().shuffleMode = 1`);
}else{
electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().shuffleMode = 0`);
}
break;
case "repeat":
electron.app.win.webContents.executeJavaScript(`wsapi.toggleRepeat()`);
break;
case "seek":
electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().seekToTime(${parseFloat(data.time)})`);
response.message = "Seek";
break;
case "pause":
electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().pause()`);
response.message = "Paused";
break;
case "play":
electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().play()`);
response.message = "Playing";
break;
case "stop":
electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().stop()`);
response.message = "Stopped";
break;
case "volume":
electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().volume = ${parseFloat(data.volume)}`);
response.message = "Volume";
break;
case "mute":
electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().mute()`);
response.message = "Muted";
break;
case "unmute":
electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().unmute()`);
response.message = "Unmuted";
break;
case "next":
electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().skipToNextItem()`);
response.message = "Next";
break;
case "previous":
electron.app.win.webContents.executeJavaScript(`MusicKit.getInstance().skipToPreviousItem()`);
response.message = "Previous";
break;
case "musickit-api":
electron.app.win.webContents.executeJavaScript(`wsapi.musickitApi(\`${data.method}\`, \`${data.id}\`, ${JSON.stringify(data.params)})`);
break;
case "musickit-library-api":
break;
case "set-autoplay":
electron.app.win.webContents.executeJavaScript(`wsapi.setAutoplay(${data.autoplay})`);
break;
case "queue-move":
electron.app.win.webContents.executeJavaScript(`wsapi.moveQueueItem(${data.from},${data.to})`);
break;
case "get-queue":
electron.app.win.webContents.executeJavaScript(`wsapi.getQueue()`);
break;
case "search":
if (!data.limit) {
data.limit = 10;
}
electron.app.win.webContents.executeJavaScript(`wsapi.search(\`${data.term}\`, \`${data.limit}\`)`);
break;
case "library-search":
if (!data.limit) {
data.limit = 10;
}
electron.app.win.webContents.executeJavaScript(`wsapi.searchLibrary(\`${data.term}\`, \`${data.limit}\`)`);
break;
case "show-window":
electron.app.win.show()
break;
case "hide-window":
electron.app.win.hide()
break;
case "play-mediaitem":
electron.app.win.webContents.executeJavaScript(`wsapi.playTrackById(${data.id}, \`${data.kind}\`)`);
response.message = "Playing track";
break;
case "get-status":
response.data = {
isAuthorized: true
};
response.message = "Status";
break;
case "get-currentmediaitem":
electron.app.win.webContents.executeJavaScript(`wsapi.getPlaybackState()`);
break;
}
ws.send(JSON.stringify(response));
});
ws.on('close', function close() {
// remove client from list
wsapi.clients.splice(wsapi.clients.indexOf(ws), 1);
console.log(`Client ${ws.id} disconnected`);
});
ws.send(JSON.stringify(defaultResponse));
});
}
sendToClient(id) {
// replace the clients.forEach with a filter to find the client that requested
}
updatePlaybackState(attr) {
const response = new standardResponse(0, attr, "OK", "playbackStateUpdate");
wsapi.clients.forEach(function each(client) {
client.send(JSON.stringify(response));
});
}
returnMusicKitApi(results, method) {
const response = new standardResponse(0, results, "OK", `musickitapi.${method}`);
wsapi.clients.forEach(function each(client) {
client.send(JSON.stringify(response));
});
}
returnDynamic(results, type) {
const response = new standardResponse(0, results, "OK", type);
wsapi.clients.forEach(function each(client) {
client.send(JSON.stringify(response));
});
}
returnLyrics(results) {
const response = new standardResponse(0, results, "OK", "lyrics");
wsapi.clients.forEach(function each(client) {
client.send(JSON.stringify(response));
});
}
returnSearch(results) {
const response = new standardResponse(0, results, "OK", "searchResults");
wsapi.clients.forEach(function each(client) {
client.send(JSON.stringify(response));
});
}
returnSearchLibrary(results) {
const response = new standardResponse(0, results, "OK", "searchResultsLibrary");
wsapi.clients.forEach(function each(client) {
client.send(JSON.stringify(response));
});
}
returnQueue(queue) {
const response = new standardResponse(0, queue, "OK", "queue");
wsapi.clients.forEach(function each(client) {
client.send(JSON.stringify(response));
});
}
}

View file

@ -1,446 +0,0 @@
const { BrowserWindow, ipcMain, shell, app, screen } = require("electron")
const { join } = require("path")
const getPort = require("get-port");
const express = require("express");
const path = require("path");
const windowStateKeeper = require("electron-window-state");
const os = require('os');
const yt = require('youtube-search-without-api-key');
const discord = require('./discordrpc');
const lastfm = require('./lastfm');
const { writeFile, writeFileSync, existsSync, mkdirSync } = require('fs');
const fs = require('fs');
const mpris = require('./mpris');
const mm = require('music-metadata');
//const mdns = require('mdns')
const qrcode = require('qrcode-terminal')
const fetch = require('electron-fetch').default;
const { Stream } = require('stream');
// Analytics for debugging.
const ElectronSentry = require("@sentry/electron");
ElectronSentry.init({ dsn: "https://68c422bfaaf44dea880b86aad5a820d2@o954055.ingest.sentry.io/6112214" });
const CiderBase = {
win: null,
requests: [],
audiostream: new Stream.PassThrough(),
async Start() {
this.clientPort = await getPort({ port: 9000 });
this.win = this.CreateBrowserWindow()
},
clientPort: 0,
CreateBrowserWindow() {
this.VerifyFiles()
// Set default window sizes
const mainWindowState = windowStateKeeper({
defaultWidth: 1024,
defaultHeight: 600
});
let win = null
const options = {
icon: join(__dirname, `../../resources/icons/icon.` + (process.platform === "win32" ? "ico" : "png")),
width: mainWindowState.width,
height: mainWindowState.height,
x: mainWindowState.x,
y: mainWindowState.y,
minWidth: 844,
minHeight: 410,
frame: false,
title: "Cider",
vibrancy: 'dark',
// transparent: true,
hasShadow: false,
webPreferences: {
webviewTag: true,
plugins: true,
nodeIntegration: true,
nodeIntegrationInWorker: false,
webSecurity: false,
allowRunningInsecureContent: true,
enableRemoteModule: true,
sandbox: true,
nativeWindowOpen: true,
contextIsolation: false,
preload: join(__dirname, '../preload/cider-preload.js')
}
}
CiderBase.InitWebServer()
// Create the BrowserWindow
win = new BrowserWindow(options)
// intercept "https://js-cdn.music.apple.com/hls.js/2.141.0/hls.js/hls.js" and redirect to local file "./apple-hls.js" instead
win.webContents.session.webRequest.onBeforeRequest({
urls: ["https://*/*.js"]
},
(details, callback) => {
if (details.url.includes("hls.js")) {
callback({
redirectURL: `http://localhost:${CiderBase.clientPort}/apple-hls.js`
})
} else {
callback({
cancel: false
})
}
}
)
win.webContents.session.webRequest.onBeforeSendHeaders(async(details, callback) => {
if (details.url === "https://buy.itunes.apple.com/account/web/info") {
details.requestHeaders['sec-fetch-site'] = 'same-site';
details.requestHeaders['DNT'] = '1';
let itspod = await win.webContents.executeJavaScript(`window.localStorage.getItem("music.ampwebplay.itspod")`)
if (itspod != null)
details.requestHeaders['Cookie'] = `itspod=${itspod}`
}
callback({ requestHeaders: details.requestHeaders })
})
win.webContents.session.webRequest.onHeadersReceived((details, callback) => {
if (details.url.match(/^https:\/\/store-\d{3}\.blobstore\.apple\.com/) || details.url.startsWith("https://store-037.blobstore.apple.com")) {
details.responseHeaders['Access-Control-Allow-Origin'] = '*';
}
callback({ responseHeaders: details.responseHeaders })
})
let location = `http://localhost:${CiderBase.clientPort}/`
win.loadURL(location)
win.on("closed", () => {
win = null
})
// Register listeners on Window to track size and position of the Window.
mainWindowState.manage(win);
// IPC stuff (senders)
ipcMain.on("cider-platform", (event) => {
event.returnValue = process.platform
})
ipcMain.on("get-gpu-mode", (event) => {
event.returnValue = process.platform
})
ipcMain.on("is-dev", (event) => {
event.returnValue = !app.isPackaged
})
// IPC stuff (listeners)
ipcMain.on('close', () => { // listen for close event
win.close();
})
ipcMain.on('put-library-songs', (event, arg) => {
fs.writeFileSync(join(app.paths.ciderCache, "library-songs.json"), JSON.stringify(arg))
})
ipcMain.on('put-library-artists', (event, arg) => {
fs.writeFileSync(join(app.paths.ciderCache, "library-artists.json"), JSON.stringify(arg))
})
ipcMain.on('put-library-albums', (event, arg) => {
fs.writeFileSync(join(app.paths.ciderCache, "library-albums.json"), JSON.stringify(arg))
})
ipcMain.on('put-library-playlists', (event, arg) => {
fs.writeFileSync(join(app.paths.ciderCache, "library-playlists.json"), JSON.stringify(arg))
})
ipcMain.on('put-library-recentlyAdded', (event, arg) => {
fs.writeFileSync(join(app.paths.ciderCache, "library-recentlyAdded.json"), JSON.stringify(arg))
})
ipcMain.on('get-library-songs', (event) => {
let librarySongs = fs.readFileSync(join(app.paths.ciderCache, "library-songs.json"), "utf8")
event.returnValue = JSON.parse(librarySongs)
})
ipcMain.on('get-library-artists', (event) => {
let libraryArtists = fs.readFileSync(join(app.paths.ciderCache, "library-artists.json"), "utf8")
event.returnValue = JSON.parse(libraryArtists)
})
ipcMain.on('get-library-albums', (event) => {
let libraryAlbums = fs.readFileSync(join(app.paths.ciderCache, "library-albums.json"), "utf8")
event.returnValue = JSON.parse(libraryAlbums)
})
ipcMain.on('get-library-playlists', (event) => {
let libraryPlaylists = fs.readFileSync(join(app.paths.ciderCache, "library-playlists.json"), "utf8")
event.returnValue = JSON.parse(libraryPlaylists)
})
ipcMain.on('get-library-recentlyAdded', (event) => {
let libraryRecentlyAdded = fs.readFileSync(join(app.paths.ciderCache, "library-recentlyAdded.json"), "utf8")
event.returnValue = JSON.parse(libraryRecentlyAdded)
})
ipcMain.handle('getYTLyrics', async(event, track, artist) => {
var u = track + " " + artist + " official video";
const videos = await yt.search(u);
return videos
})
ipcMain.handle('getStoreValue', (event, key, defaultValue) => {
return (defaultValue ? app.cfg.get(key, true) : app.cfg.get(key));
});
ipcMain.handle('setStoreValue', (event, key, value) => {
app.cfg.set(key, value);
});
ipcMain.on('getStore', (event) => {
event.returnValue = app.cfg.store
})
ipcMain.on('setStore', (event, store) => {
app.cfg.store = store
})
ipcMain.handle('setVibrancy', (event, key, value) => {
win.setVibrancy(value)
});
ipcMain.on('maximize', () => { // listen for maximize event
if (win.isMaximized()) {
win.unmaximize()
} else {
win.maximize()
}
})
ipcMain.on('minimize', () => { // listen for minimize event
win.minimize();
})
ipcMain.on('setFullScreen', (event, flag) => {
win.setFullScreen(flag)
})
if (process.platform === "win32") {
let WND_STATE = {
MINIMIZED: 0,
NORMAL: 1,
MAXIMIZED: 2,
FULL_SCREEN: 3
}
let wndState = WND_STATE.NORMAL
win.on("resize", (_event) => {
const isMaximized = win.isMaximized()
const isMinimized = win.isMinimized()
const isFullScreen = win.isFullScreen()
const state = wndState;
if (isMinimized && state !== WND_STATE.MINIMIZED) {
wndState = WND_STATE.MINIMIZED
} else if (isFullScreen && state !== WND_STATE.FULL_SCREEN) {
wndState = WND_STATE.FULL_SCREEN
} else if (isMaximized && state !== WND_STATE.MAXIMIZED) {
wndState = WND_STATE.MAXIMIZED
win.webContents.executeJavaScript(`app.chrome.maximized = true`)
} else if (state !== WND_STATE.NORMAL) {
wndState = WND_STATE.NORMAL
win.webContents.executeJavaScript(`app.chrome.maximized = false`)
}
})
}
// Set window Handler
win.webContents.setWindowOpenHandler(({ url }) => {
if (url.includes("apple") || url.includes("localhost")) {
return { action: "allow" }
}
shell.openExternal(url).catch(() => {})
return {
action: 'deny'
}
})
// Set scale
ipcMain.on('setScreenScale', (event, scale) => {
win.webContents.setZoomFactor(parseFloat(scale))
})
win.webContents.setZoomFactor(screen.getPrimaryDisplay().scaleFactor)
mpris.connect(win)
mpris.SetButtons(win, false)
lastfm.authenticate()
// Discord
discord.connect((app.cfg.get("general.discord_rpc") == 1) ? '911790844204437504' : '886578863147192350');
ipcMain.on('playbackStateDidChange', (_event, a) => {
app.media = a;
discord.updateActivity(a)
mpris.SetButtons(win, a)
mpris.updateState(a)
lastfm.scrobbleSong(a)
lastfm.updateNowPlayingSong(a)
});
ipcMain.on('nowPlayingItemDidChange', (_event, a) => {
app.media = a;
discord.updateActivity(a)
mpris.SetButtons(win, a)
mpris.updateAttributes(a)
lastfm.scrobbleSong(a)
lastfm.updateNowPlayingSong(a)
});
ipcMain.on("getPreviewURL", (_event, url) => {
fetch(url)
.then(res => res.buffer())
.then(async(buffer) => {
try {
const metadata = await mm.parseBuffer(buffer, 'audio/x-m4a');
SoundCheckTag = metadata.native.iTunes[1].value
win.webContents.send('SoundCheckTag', SoundCheckTag)
} catch (error) {
console.error(error.message);
}
})
});
ipcMain.on('writeAudio', function(event, buffer) {
CiderBase.audiostream.write(Buffer.from(buffer));
})
return win
},
VerifyFiles() {
const expectedDirectories = [
"CiderCache"
]
const expectedFiles = [
"library-songs.json",
"library-artists.json",
"library-albums.json",
"library-playlists.json",
"library-recentlyAdded.json",
]
for (let i = 0; i < expectedDirectories.length; i++) {
if (!existsSync(path.join(app.getPath("userData"), expectedDirectories[i]))) {
mkdirSync(path.join(app.getPath("userData"), expectedDirectories[i]))
}
}
for (let i = 0; i < expectedFiles.length; i++) {
const file = path.join(app.paths.ciderCache, expectedFiles[i])
if (!existsSync(file)) {
writeFileSync(file, JSON.stringify([]))
}
}
},
EnvironmentVariables: {
"env": {
platform: os.platform(),
dev: app.isPackaged
}
},
LinkHandler: (startArgs) => {
if (!startArgs) return;
if (String(startArgs).includes('auth')) {
let authURI = String(startArgs).split('/auth/')[1]
if (authURI.startsWith('lastfm')) { // If we wanted more auth options
console.log("lfmtoken", String(startArgs))
const authKey = authURI.split('lastfm?token=')[1];
app.cfg.set('lastfm.enabled', true);
app.cfg.set('lastfm.auth_token', authKey);
CiderBase.win.webContents.send('LastfmAuthenticated', authKey);
lastfm.authenticate()
}
} else {
if (String(startArgs).includes('/play/')) { //Steer away from protocal:// specific conditionals
const playParam = String(startArgs).split('/play/')[1]
if (playParam.includes('s/')) { // setQueue can be done with album, song, url, playlist id
console.log(playParam)
let song = playParam.split('s/')[1]
console.warn(`[LinkHandler] Attempting to load song by id: ${song}`);
this.win.webContents.executeJavaScript(`
MusicKit.getInstance().setQueue({ song: '${song}'}).then(function(queue) {
MusicKit.getInstance().play();
});
`).catch((err) => console.error(err));
}
}
}
},
async InitWebServer() {
const webapp = express();
const webRemotePath = path.join(__dirname, '../renderer/');
webapp.set("views", path.join(webRemotePath, "views"));
webapp.set("view engine", "ejs");
let firstRequest = true
//const webRemoteMDNS = mdns.createAdvertisement(mdns.tcp('https'), 9000, { name: "cider", domain: 'local' })
//webRemoteMDNS.start()
//* Prep for remote -quack
webapp.use(function(req, res, next) {
// if not localhost
if (req.url.includes("audio.webm") || (req.headers.host.includes("localhost") && req.headers["user-agent"].includes("Cider"))) {
next();
} else {
console.log(req.get('host'))
res.redirect("https://discord.gg/applemusic")
}
});
webapp.use(express.static(webRemotePath));
webapp.get('/', function(req, res) {
//if (!req.headers["user-agent"].includes("Cider"))
//res.sendFile(path.join(webRemotePath, 'index_old.html'));
if (firstRequest) {
console.log("---- Ignore Me ;) ---")
qrcode.generate(`http://${os.hostname}:9000`) //Prep for remote
console.log("---- Ignore Me ;) ---")
/*
*
* USING https://www.npmjs.com/package/qrcode-terminal for terminal
* WE SHOULD USE https://www.npmjs.com/package/qrcode for the remote (or others)
* -quack
*/
}
firstRequest = false
res.render("main", CiderBase.EnvironmentVariables)
});
webapp.get('/audio.webm', function(req, res) {
console.log('hi')
try {
req.connection.setTimeout(Number.MAX_SAFE_INTEGER);
// CiderBase.requests.push({req: req, res: res});
// var pos = CiderBase.requests.length - 1;
// req.on("close", () => {
// console.info("CLOSED", CiderBase.requests.length);
// requests.splice(pos, 1);
// console.info("CLOSED", CiderBase.requests.length);
// });
CiderBase.audiostream.on('data', (data) => {
try {
res.write(data);
} catch (ex) {
console.log(ex)
}
})
} catch (ex) { console.log(ex) }
});
webapp.listen(CiderBase.clientPort, function() {
console.log(`Cider hosted on: ${CiderBase.clientPort}`);
});
},
}
module.exports = CiderBase;

View file

@ -1,142 +0,0 @@
const { app } = require('electron'),
DiscordRPC = require('discord-rpc')
module.exports = {
/**
* Connects to Discord RPC
* @param {string} clientId
*/
connect: function(clientId) {
app.discord = { isConnected: false };
if (app.cfg.get('general.discord_rpc') == 0 || app.discord.isConnected) return;
DiscordRPC.register(clientId) // Apparently needed for ask to join, join, spectate etc.
const client = new DiscordRPC.Client({ transport: "ipc" });
app.discord = Object.assign(client, { error: false, activityCache: null, isConnected: false });
// Login to Discord
app.discord.login({ clientId })
.then(() => {
app.discord.isConnected = true;
})
.catch((e) => console.error(`[DiscordRPC][connect] ${e}`));
app.discord.on('ready', () => {
console.log(`[DiscordRPC][connect] Successfully Connected to Discord. Authed for user: ${client.user.username} (${client.user.id})`);
})
// Handles Errors
app.discord.on('error', err => {
console.error(`[DiscordRPC] ${err}`);
this.disconnect()
app.discord.isConnected = false;
});
},
/**
* Disconnects from Discord RPC
*/
disconnect: function() {
if (app.cfg.get('general.discord_rpc') == 0 || !app.discord.isConnected) return;
try {
app.discord.destroy().then(() => {
app.discord.isConnected = false;
console.log('[DiscordRPC][disconnect] Disconnected from discord.')
}).catch((e) => console.error(`[DiscordRPC][disconnect] ${e}`));
} catch (err) {
console.error(err)
}
},
/**
* Sets the activity of the client
* @param {object} attributes
*/
updateActivity: function(attributes) {
if (app.cfg.get('general.discord_rpc') == 0) return;
if (!app.discord.isConnected) {
app.discord.clearActivity().catch((e) => console.error(`[DiscordRPC][updateActivity] ${e}`));
return;
}
// console.log('[DiscordRPC][updateActivity] Updating Discord Activity.')
const listenURL = `https://cider.sh/p?s&id=${attributes.playParams.id}` // cider://play/s/[id] (for song)
//console.log(attributes)
let ActivityObject = {
details: attributes.name,
state: `by ${attributes.artistName}`,
startTimestamp: attributes.startTime,
endTimestamp: attributes.endTime,
largeImageKey: (attributes.artwork.url.replace('{w}', '1024').replace('{h}', '1024')) ?? 'cider',
largeImageText: attributes.albumName,
smallImageKey: (attributes.status ? 'play' : 'pause'),
smallImageText: (attributes.status ? 'Playing' : 'Paused'),
instance: true,
buttons: [
{ label: "Listen on Cider", url: listenURL },
]
};
if (ActivityObject.largeImageKey == "" || ActivityObject.largeImageKey == null) {
ActivityObject.largeImageKey = (app.cfg.get("general.discord_rpc") == 1) ? "cider" : "logo"
}
// Remove the pause/play icon and test for clear activity on pause
if (app.cfg.get('general.discordClearActivityOnPause') == 1) {
delete ActivityObject.smallImageKey
delete ActivityObject.smallImageText
}
// Deletes the timestamp if its not greater than 0
if (!((new Date(attributes.endTime)).getTime() > 0)) {
delete ActivityObject.startTimestamp
delete ActivityObject.endTimestamp
}
// Artist check
if (!attributes.artistName) {
delete ActivityObject.state
}
// Album text check
if (!ActivityObject.largeImageText || ActivityObject.largeImageText.length < 2) {
delete ActivityObject.largeImageText
}
// Checks if the name is greater than 128 because some songs can be that long
if (ActivityObject.details.length > 128) {
ActivityObject.details = ActivityObject.details.substring(0, 125) + '...'
}
// Check if its pausing (false) or playing (true)
if (!attributes.status) {
if (app.cfg.get('general.discordClearActivityOnPause') == 1) {
app.discord.clearActivity().catch((e) => console.error(`[DiscordRPC][clearActivity] ${e}`));
ActivityObject = null
} else {
delete ActivityObject.startTimestamp
delete ActivityObject.endTimestamp
ActivityObject.smallImageKey = 'pause'
ActivityObject.smallImageText = 'Paused'
}
}
if (ActivityObject && ActivityObject !== app.discord.activityCache && ActivityObject.details && ActivityObject.state) {
try {
// console.log(`[DiscordRPC][setActivity] Setting activity to ${JSON.stringify(ActivityObject)}`);
app.discord.setActivity(ActivityObject)
app.discord.activityCache = ActivityObject
} catch (err) {
console.error(`[DiscordRPC][setActivity] ${err}`)
}
}
},
}

116
src/main/index.ts Normal file
View file

@ -0,0 +1,116 @@
require('v8-compile-cache');
// Analytics for debugging fun yeah.
const ElectronSentry = require("@sentry/electron");
ElectronSentry.init({dsn: "https://68c422bfaaf44dea880b86aad5a820d2@o954055.ingest.sentry.io/6112214"});
import * as electron from 'electron';
import {Win} from "./base/win";
import {ConfigStore} from "./base/store";
import {AppEvents} from "./base/app";
import PluginHandler from "./base/plugins";
// const test = new PluginHandler();
const config = new ConfigStore();
const App = new AppEvents(config.store);
const Cider = new Win(electron.app, config.store)
const plug = new PluginHandler();
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* App Event Handlers
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
electron.app.on('ready', () => {
App.ready();
console.log('[Cider] Application is Ready. Creating Window.')
if (!electron.app.isPackaged) {
console.info('[Cider] Running in development mode.')
require('vue-devtools').install()
}
electron.components.whenReady().then(() => {
Cider.createWindow().then((win) => {
plug.callPlugins('onReady', win);
})
})
});
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Renderer Event Handlers
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
electron.ipcMain.on('playbackStateDidChange', (event, attributes) => {
plug.callPlugins('onPlaybackStateDidChange', attributes);
});
electron.ipcMain.on('nowPlayingItemDidChange', (event, attributes) => {
plug.callPlugins('onNowPlayingItemDidChange', attributes);
});
//
electron.app.on('before-quit', () => {
plug.callPlugins('onBeforeQuit');
console.warn(`${electron.app.getName()} exited.`);
});
//
// // @ts-ignore
// // Widevine Stuff
// electron.app.on('widevine-ready', (version, lastVersion) => {
// if (null !== lastVersion) {
// console.log('[Cider][Widevine] Widevine ' + version + ', upgraded from ' + lastVersion + ', is ready to be used!')
// } else {
// console.log('[Cider][Widevine] Widevine ' + version + ' is ready to be used!')
// }
// })
// // @ts-ignore
// electron.app.on('widevine-update-pending', (currentVersion, pendingVersion) => {
// console.log('[Cider][Widevine] Widevine ' + currentVersion + ' is ready to be upgraded to ' + pendingVersion + '!')
// })
// // @ts-ignore
// electron.app.on('widevine-error', (error) => {
// console.log('[Cider][Widevine] Widevine installation encountered an error: ' + error)
// electron.app.exit()
// })
//
//
// app.on('open-url', (event, url) => {
// event.preventDefault()
// if (url.includes('ame://') || url.includes('itms://') || url.includes('itmss://') || url.includes('musics://') || url.includes('music://')) {
// CiderBase.LinkHandler(url)
// }
// })
//
// app.on('second-instance', (_e, argv) => {
// console.warn(`[InstanceHandler][SecondInstanceHandler] Second Instance Started with args: [${argv.join(', ')}]`)
//
// // Checks if first instance is authorized and if second instance has protocol args
// argv.forEach((value) => {
// if (value.includes('ame://') || value.includes('itms://') || value.includes('itmss://') || value.includes('musics://') || value.includes('music://')) {
// console.warn(`[InstanceHandler][SecondInstanceHandler] Found Protocol!`)
// CiderBase.LinkHandler(value);
// }
// })
//
// if (argv.includes("--force-quit")) {
// console.warn('[InstanceHandler][SecondInstanceHandler] Force Quit found. Quitting App.');
// app.isQuiting = true
// app.quit()
// } else if (app.win && !app.cfg.get('advanced.allowMultipleInstances')) { // If a Second Instance has Been Started
// console.warn('[InstanceHandler][SecondInstanceHandler] Showing window.');
// app.win.show()
// app.win.focus()
// }
// })
//
// if (!app.requestSingleInstanceLock() && !app.cfg.get('advanced.allowMultipleInstances')) {
// console.warn("[InstanceHandler] Existing Instance is Blocking Second Instance.");
// app.quit();
// app.isQuiting = true
// }

View file

@ -1,153 +0,0 @@
const {app, Notification} = require('electron'),
fs = require('fs'),
{resolve} = require('path'),
sessionPath = resolve(app.getPath('userData'), 'session.json'),
apiCredentials = require('../../resources/lfmApiCredentials.json'),
LastfmAPI = require('lastfmapi');
const lfm = {
authenticateFromFile: function () {
let sessionData = require(sessionPath)
console.log("[LastFM][authenticateFromFile] Logging in with Session Info.")
app.lastfm.setSessionCredentials(sessionData.name, sessionData.key)
console.log("[LastFM][authenticateFromFile] Logged in.")
},
authenticate: function () {
if (app.cfg.get('lastfm.auth_token')) {
app.cfg.set('lastfm.enabled', true);
}
if (!app.cfg.get('lastfm.enabled') || !app.cfg.get('lastfm.auth_token')) {
app.cfg.set('lastfm.enabled', false);
return
}
const lfmAPI = new LastfmAPI({
'api_key': apiCredentials.key,
'secret': apiCredentials.secret
});
app.lastfm = Object.assign(lfmAPI, {cachedAttributes: false, cachedNowPlayingAttributes: false});
fs.stat(sessionPath, function (err) {
if (err) {
console.error("[LastFM][Session] Session file couldn't be opened or doesn't exist,", err)
console.log("[LastFM][Auth] Beginning authentication from configuration")
app.lastfm.authenticate(app.cfg.get('lastfm.auth_token'), function (err, session) {
if (err) {
throw err;
}
console.log("[LastFM] Successfully obtained LastFM session info,", session); // {"name": "LASTFM_USERNAME", "key": "THE_USER_SESSION_KEY"}
console.log("[LastFM] Saving session info to disk.")
let tempData = JSON.stringify(session)
fs.writeFile(sessionPath, tempData, (err) => {
if (err)
console.log("[LastFM][fs]", err)
else {
console.log("[LastFM][fs] File was written successfully.")
lfm.authenticateFromFile()
new Notification({
title: app.getName(),
body: "Successfully logged into LastFM using Authentication Key."
}).show()
}
})
});
} else {
lfm.authenticateFromFile()
}
})
},
scrobbleSong: async function (attributes) {
await new Promise(resolve => setTimeout(resolve, Math.round(attributes.durationInMillis * (app.cfg.get('lastfm.scrobble_after') / 100))));
const currentAttributes = app.media;
if (!app.lastfm || app.lastfm.cachedAttributes === attributes ) {
return
}
if (app.lastfm.cachedAttributes) {
if (app.lastfm.cachedAttributes.playParams.id === attributes.playParams.id) return;
}
if (currentAttributes.status && currentAttributes === attributes) {
if (fs.existsSync(sessionPath)) {
// Scrobble playing song.
if (attributes.status === true) {
app.lastfm.track.scrobble({
'artist': lfm.filterArtistName(attributes.artistName),
'track': attributes.name,
'album': attributes.albumName,
'albumArtist': this.filterArtistName(attributes.artistName),
'timestamp': new Date().getTime() / 1000
}, function (err, scrobbled) {
if (err) {
return console.error('[LastFM] An error occurred while scrobbling', err);
}
console.log('[LastFM] Successfully scrobbled: ', scrobbled);
});
app.lastfm.cachedAttributes = attributes
}
} else {
this.authenticate();
}
} else {
return console.log('[LastFM] Did not add ', attributes.name , '—' , lfm.filterArtistName(attributes.artistName), 'because now playing a other song.');
}
},
filterArtistName: function (artist) {
if (!app.cfg.get('lastfm.enabledRemoveFeaturingArtists')) return artist;
artist = artist.split(' ');
if (artist.includes('&')) {
artist.length = artist.indexOf('&');
}
if (artist.includes('and')) {
artist.length = artist.indexOf('and');
}
artist = artist.join(' ');
if (artist.includes(',')) {
artist = artist.split(',')
artist = artist[0]
}
return artist.charAt(0).toUpperCase() + artist.slice(1);
},
updateNowPlayingSong: function (attributes) {
if (!app.lastfm ||app.lastfm.cachedNowPlayingAttributes === attributes | !app.cfg.get('lastfm.NowPlaying')) {
return
}
if (app.lastfm.cachedNowPlayingAttributes) {
if (app.lastfm.cachedNowPlayingAttributes.playParams.id === attributes.playParams.id) return;
}
if (fs.existsSync(sessionPath)) {
// update Now Playing
if (attributes.status === true) {
app.lastfm.track.updateNowPlaying({
'artist': lfm.filterArtistName(attributes.artistName),
'track': attributes.name,
'album': attributes.albumName,
'albumArtist': this.filterArtistName(attributes.artistName)
}, function (err, nowPlaying) {
if (err) {
return console.error('[LastFM] An error occurred while updating nowPlayingSong', err);
}
console.log('[LastFM] Successfully updated nowPlayingSong', nowPlaying);
});
app.lastfm.cachedNowPlayingAttributes = attributes
}
} else {
this.authenticate()
}
}
}
module.exports = lfm;

View file

@ -1,149 +0,0 @@
const { nativeImage } = require("electron");
const path = require('path')
let mediaPlayer = null;
module.exports = {
/**
* Connects to the MPRIS interface.
* @param {Object} win - The BrowserWindow.
*/
connect: (win) => {
if (process.platform !== "linux") return;
const Player = require('mpris-service');
mediaPlayer = Player({
name: 'Cider',
identity: 'Cider',
supportedUriSchemes: [],
supportedMimeTypes: [],
supportedInterfaces: ['player']
});
mediaPlayer = Object.assign(mediaPlayer, { canQuit: true, canControl: true, canPause: true, canPlay: true, canGoNext: true })
let pos_atr = {durationInMillis: 0};
mediaPlayer.getPosition = function () {
const durationInMicro = pos_atr.durationInMillis * 1000;
const percentage = parseFloat("0") || 0;
return durationInMicro * percentage;
}
mediaPlayer.active = true
mediaPlayer.on('playpause', async () => {
win.webContents.executeJavaScript('MusicKitInterop.pausePlay()').catch(err => console.error(err))
});
mediaPlayer.on('play', async () => {
win.webContents.executeJavaScript('MusicKitInterop.pausePlay()').catch(err => console.error(err))
});
mediaPlayer.on('pause', async () => {
win.webContents.executeJavaScript('MusicKitInterop.pausePlay()').catch(err => console.error(err))
});
mediaPlayer.on('next', async () => {
win.webContents.executeJavaScript('MusicKitInterop.nextTrack()').catch(err => console.error(err))
});
mediaPlayer.on('previous', async () => {
win.webContents.executeJavaScript('MusicKitInterop.previousTrack()').catch(err => console.error(err))
});
},
/**
* Updates the MPRIS interface.
* @param {Object} attributes - The attributes of the track.
*/
updateAttributes: (attributes) => {
if (process.platform !== "linux") return;
const MetaData = {
'mpris:trackid': mediaPlayer.objectPath(`track/${attributes.playParams.id.replace(/[.]+/g, "")}`),
'mpris:length': attributes.durationInMillis * 1000, // In microseconds
'mpris:artUrl': (attributes.artwork.url.replace('/{w}x{h}bb', '/512x512bb')).replace('/2000x2000bb', '/35x35bb'),
'xesam:title': `${attributes.name}`,
'xesam:album': `${attributes.albumName}`,
'xesam:artist': [`${attributes.artistName}`,],
'xesam:genre': attributes.genreNames
}
if (mediaPlayer.metadata["mpris:trackid"] === MetaData["mpris:trackid"]) {
return
}
mediaPlayer.metadata = MetaData
},
/**
* Updates the playback state of the MPRIS interface.
* @param {Object} attributes - The attributes of the track.
*/
updateState: (attributes) => {
if (process.platform !== "linux") return;
function setPlaybackIfNeeded(status) {
if (mediaPlayer.playbackStatus === status) {
return
}
mediaPlayer.playbackStatus = status;
}
switch (attributes.status) {
case true: // Playing
setPlaybackIfNeeded('Playing');
break;
case false: // Paused
setPlaybackIfNeeded('Paused');
break;
default: // Stopped
setPlaybackIfNeeded('Stopped');
break;
}
},
SetButtons: (win, attributes) => {
if (process.platform === 'win32') { // Set the Windows Thumbnail Toolbar Buttons
win.setThumbarButtons([
{
tooltip: 'Previous',
icon: nativeImage.createFromPath(path.join(__dirname, 'thumbaricons/backwardPng.png')),
click() {
console.log("Clicked the bc taskbar button!")
win.webContents.executeJavaScript('MusicKitInterop.previousTrack()').catch(err => console.error(err))
}
},
{
tooltip: attributes.status ? 'Pause' : 'Play',
//tooltip: 'Play',
icon: attributes.status ? nativeImage.createFromPath(path.join(__dirname, 'thumbaricons/pausePng.png')) : nativeImage.createFromPath(path.join(__dirname, 'thumbaricons/playPng.png')),
click() {
console.log("Clicked the pl taskbar button!")
win.webContents.executeJavaScript('MusicKitInterop.pausePlay()').catch(err => console.error(err))
}
},
{
tooltip: 'Next',
icon: nativeImage.createFromPath(path.join(__dirname, 'thumbaricons/forwardPng.png')),
click() {
console.log("Clicked the fw taskbar button!")
win.webContents.executeJavaScript('MusicKitInterop.nextTrack()').catch(err => console.error(err))
}
}
]);
}
},
/**
* Closes the MPRIS interface.
*/
clearActivity: () => {
if (process.platform !== "linux") return;
mediaPlayer.metadata = {'mpris:trackid': '/org/mpris/MediaPlayer2/TrackList/NoTrack'}
mediaPlayer.playbackStatus = 'Stopped';
},
}

View file

@ -0,0 +1,58 @@
let i = 1, k = 1;
export default class ExamplePlugin {
/**
* Private variables for interaction in plugins
*/
private _win: any;
private _app: any;
/**
* Base Plugin Details (Eventually implemented into a GUI in settings)
*/
public name: string = 'examplePlugin';
public description: string = 'Example plugin';
public version: string = '1.0.0';
public author: string = 'Example author';
/**
* Runs on plugin load (Currently run on application start)
*/
constructor(app: any) {
this._app = app;
console.log('Example plugin loaded');
}
/**
* Runs on app ready
*/
onReady(win: any): void {
this._win = win;
console.log('Example plugin ready');
}
/**
* Runs on app stop
*/
onBeforeQuit(): void {
console.log('Example plugin stopped');
}
/**
* Runs on playback State Change
* @param attributes Music Attributes (attributes.state = current state)
*/
onPlaybackStateDidChange(attributes: object): void {
console.log('onPlaybackStateDidChange has been called ' + i +' times');
i++
}
/**
* Runs on song change
* @param attributes Music Attributes
*/
onNowPlayingItemDidChange(attributes: object): void {
console.log('onNowPlayingDidChange has been called ' + k +' times');
k++
}
}

View file

@ -0,0 +1,39 @@
import * as electron from "electron";
export default class sendSongToTitlebar {
/**
* Base Plugin Details (Eventually implemented into a GUI in settings)
*/
public name: string = 'sendSongToTitlebar';
public description: string = 'Sets the app\'s titlebar to the Song title';
public version: string = '0.0.1';
public author: string = 'Cider Collective (credit to 8times9 via #147)';
/**
* Runs on plugin load (Currently run on application start)
*/
private _win: any;
private _app: any;
constructor() {}
/**
* Runs on app ready
*/
onReady(win: any): void {
this._win = win;
}
/**
* Runs on app stop
*/
onBeforeQuit(): void {}
/**
* Runs on playback State Change
* @param attributes Music Attributes (attributes.state = current state)
*/
onPlaybackStateDidChange(attributes: any): void {
this._win.setTitle(`${(attributes != null && attributes.name != null && attributes.name.length > 0) ? (attributes.name + " - ") : ''}Cider`)
}
/**
* Runs on song change
* @param attributes Music Attributes
*/
onNowPlayingItemDidChange(attributes: object): void {}
}

View file

@ -1,5 +1,4 @@
const electron = require('electron')
global.ipcRenderer = require('electron').ipcRenderer;
console.log('Loaded Preload')
let cache = {playParams: {id: 0}, status: null, remainingTime: 0},
@ -97,6 +96,5 @@ const MusicKitInterop = {
process.once('loaded', () => {
console.log("Setting ipcRenderer")
global.ipcRenderer = electron.ipcRenderer;
global.MusicKitInterop = MusicKitInterop;
});

View file

@ -0,0 +1,5 @@
{
"js": {
"beautify.ignore": "src/renderer/index.js"
}
}

View file

@ -0,0 +1,103 @@
const wsapi = {
cache: {playParams: {id: 0}, status: null, remainingTime: 0},
playbackCache: {status: null, time: Date.now()},
search(term, limit) {
MusicKit.getInstance().api.search(term, {limit: limit, types: 'songs,artists,albums'}).then((results)=>{
ipcRenderer.send('wsapi-returnSearch', JSON.stringify(results))
})
},
searchLibrary(term, limit) {
MusicKit.getInstance().api.library.search(term, {limit: limit, types: 'library-songs,library-artists,library-albums'}).then((results)=>{
ipcRenderer.send('wsapi-returnSearchLibrary', JSON.stringify(results))
})
},
getAttributes: function () {
const mk = MusicKit.getInstance();
const nowPlayingItem = mk.nowPlayingItem;
const isPlayingExport = mk.isPlaying;
const remainingTimeExport = mk.currentPlaybackTimeRemaining;
const attributes = (nowPlayingItem != null ? nowPlayingItem.attributes : {});
attributes.status = isPlayingExport ? isPlayingExport : false;
attributes.name = attributes.name ? attributes.name : 'No Title Found';
attributes.artwork = attributes.artwork ? attributes.artwork : {url: ''};
attributes.artwork.url = attributes.artwork.url ? attributes.artwork.url : '';
attributes.playParams = attributes.playParams ? attributes.playParams : {id: 'no-id-found'};
attributes.playParams.id = attributes.playParams.id ? attributes.playParams.id : 'no-id-found';
attributes.albumName = attributes.albumName ? attributes.albumName : '';
attributes.artistName = attributes.artistName ? attributes.artistName : '';
attributes.genreNames = attributes.genreNames ? attributes.genreNames : [];
attributes.remainingTime = remainingTimeExport ? (remainingTimeExport * 1000) : 0;
attributes.durationInMillis = attributes.durationInMillis ? attributes.durationInMillis : 0;
attributes.startTime = Date.now();
attributes.endTime = attributes.endTime ? attributes.endTime : Date.now();
attributes.volume = mk.volume;
attributes.shuffleMode = mk.shuffleMode;
attributes.repeatMode = mk.repeatMode;
attributes.autoplayEnabled = mk.autoplayEnabled;
return attributes
},
moveQueueItem(oldPosition, newPosition) {
MusicKit.getInstance().queue._queueItems.splice(newPosition,0,MusicKit.getInstance().queue._queueItems.splice(oldPosition,1)[0])
MusicKit.getInstance().queue._reindex()
},
setAutoplay(value) {
MusicKit.getInstance().autoplayEnabled = value
},
returnDynamic(data, type) {
ipcRenderer.send('wsapi-returnDynamic', JSON.stringify(data), type)
},
musickitApi(method, id, params) {
MusicKit.getInstance().api[method](id, params).then((results)=>{
ipcRenderer.send('wsapi-returnMusicKitApi', JSON.stringify(results), method)
})
},
getPlaybackState () {
ipcRenderer.send('wsapi-updatePlaybackState', MusicKitInterop.getAttributes());
},
getLyrics() {
return []
_lyrics.GetLyrics(1, false)
},
getQueue() {
ipcRenderer.send('wsapi-returnQueue', JSON.stringify(MusicKit.getInstance().queue))
},
playNext(type, id) {
var request = {}
request[type] = id
MusicKit.getInstance().playNext(request)
},
playLater(type, id) {
var request = {}
request[type] = id
MusicKit.getInstance().playLater(request)
},
love() {
},
playTrackById(id, kind = "song") {
MusicKit.getInstance().setQueue({ [kind]: id }).then(function (queue) {
MusicKit.getInstance().play()
})
},
quickPlay(term) {
// Quick play by song name
MusicKit.getInstance().api.search(term, { limit: 2, types: 'songs' }).then(function (data) {
MusicKit.getInstance().setQueue({ song: data["songs"][0]["id"] }).then(function (queue) {
MusicKit.getInstance().play()
})
})
},
toggleShuffle() {
MusicKit.getInstance().shuffleMode = MusicKit.getInstance().shuffleMode === 0 ? 1 : 0
},
toggleRepeat() {
if(MusicKit.getInstance().repeatMode == 0) {
MusicKit.getInstance().repeatMode = 2
}else if(MusicKit.getInstance().repeatMode == 2){
MusicKit.getInstance().repeatMode = 1
}else{
MusicKit.getInstance().repeatMode = 0
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -18,6 +18,7 @@
--navbarHeight: 48px;
--selected: rgb(130 130 130 / 30%);
--selected-click: rgb(80 80 80 / 30%);
--hover: rgb(200 200 200 / 10%);
--keyColor: #fa586a;
--keyColor-rgb: 250, 88, 106;
--keyColor-rollover: #ff8a9c;
@ -254,6 +255,32 @@ input[type="text"], input[type="number"] {
}
}
.artworkMaterial {
position: relative;
height:100%;
width:100%;
overflow: hidden;
pointer-events: none;
>img {
position: absolute;
width: 200%;
opacity: 0.5;
filter: brightness(200%) blur(180px) saturate(280%) contrast(2);
}
>img:first-child {
top:0;
left:0;
}
>img:last-child {
bottom:0;
right: 0;
transform: rotate(180deg);
}
}
[artwork-hidden] {
transition: opacity .25s var(--appleEase);
@ -1105,6 +1132,34 @@ input[type=range].web-slider::-webkit-slider-runnable-track {
justify-content: center;
align-items: center;
filter: contrast(0.8);
.lcdMenu {
height: 100%;
width: 100%;
padding: 0px;
margin: 0px;
background: transparent;
border: 0px;
appearance: none;
display: flex;
justify-content: center;
align-items: center;
border-radius: 6px;
&:focus {
outline: none;
}
&:hover {
background: var(--hover);
}
&:active {
background: var(--selected-click);
transform: scale(0.95);
}
.svg-icon {
--url: url('views/svg/more.svg')!important;
}
}
}
.app-chrome .app-chrome-item > .app-playback-controls .playback-info {
@ -1896,6 +1951,36 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb {
/* Cider */
.more-btn-round {
border-radius: 100%;
background: rgba(100, 100, 100, 0.5);
box-shadow: var(--ciderShadow-Generic);
width: 32px;
height: 32px;
border: 0px;
cursor: pointer;
z-index: 5;
display: flex;
justify-content: center;
align-items: center;
&:hover {
filter: brightness(125%);
}
&:active {
filter: brightness(75%);
transform: scale(0.98);
transition: transform 0s var(--appleEase), box-shadow 0.2s var(--appleEase);
}
.svg-icon {
width: 100%;
background: #eee;
--url: url("./views/svg/more.svg");
}
}
.about-page {
.teamBtn {
display: flex;
@ -1951,6 +2036,14 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb {
&.md-btn-block {
display: block;
width:100%;
}
&.md-btn-glyph {
display:flex;
align-items: center;
justify-content: center;
width: 100%;
}
&.md-btn-primary {
@ -2398,115 +2491,171 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb {
.playlist-page {
--bgColor: transparent;
padding: 0px;
background: linear-gradient(180deg, var(--bgColor) 32px, var(--bgColor) 59px, transparent 60px, transparent 100%);
//background: linear-gradient(180deg, var(--bgColor) 32px, var(--bgColor) 18px, transparent 60px, transparent 100%);
top: 0;
padding-top: var(--navigationBarHeight);
.playlist-body {
padding: var(--contentInnerPadding);
padding: 0px var(--contentInnerPadding) 0px var(--contentInnerPadding);
}
.floating-header {
position: sticky;
top: 0;
left: 0;
border-bottom: 1px solid rgba(200, 200, 200, 0.05);
z-index: 6;
padding: 0px 1em;
backdrop-filter: blur(32px);
background: rgba(24, 24, 24, 0.15);
top: var(--navigationBarHeight);
transition: opacity 0.1s var(--appleEase);
}
.playlist-display {
padding: var(--contentInnerPadding);
min-height: 300px;
position: relative;
.playlist-info {
flex-shrink: unset;
.artworkContainer {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
margin: 0;
padding: 0;
-webkit-mask-image: radial-gradient(at top left, black, transparent 70%), radial-gradient(at top right, black, transparent 70%), linear-gradient(180deg, rgb(200 200 200), transparent 98%);
opacity: .7;
animation: playlistArtworkFadeIn 1s var(--appleEase);
.artworkMaterial>img {
filter: brightness(100%) blur(80px) saturate(100%) contrast(1);
object-position: center;
object-fit: cover;
width: 100%;
height: 100%;
transform: unset;
}
}
.playlistInfo {
z-index: 1;
position: absolute;
bottom: 0;
left: 0;
right: 0;
top: 0;
display: flex;
flex-flow: column;
justify-content: flex-end;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
.playlist-name {
font-weight: 700;
font-size: 1.6rem;
margin-bottom: 6px;
margin-right: 6px;
flex-shrink: unset;
>.row {
width: calc(100% - 32px);
}
.nameEdit {
font-weight: 700;
font-size: 1.6rem;
margin-bottom: 6px;
margin-right: 6px;
.playlist-info {
flex-shrink: unset;
background: transparent;
border: 0px;
color: inherit;
font-family: inherit;
}
display: flex;
flex-flow: column;
justify-content: flex-end;
.playlist-artist {
font-size: 20px;
margin-bottom: 6px;
margin-right: 6px;
flex-shrink: unset;
}
.playlist-desc {
box-sizing: border-box;
font-size: 14px;
flex-shrink: unset;
margin-right: 5px;
max-height: 100px;
position: relative;
.content {
height: 100px;
-webkit-mask-image: -webkit-gradient(linear, left 50%, left 90%, from(rgba(0, 0, 0, 1)), to(rgba(0, 0, 0, 0)));
.playlist-name {
font-weight: 700;
font-size: 1.6rem;
margin-bottom: 6px;
margin-right: 6px;
flex-shrink: unset;
}
.more-btn {
appearance: none;
position: absolute;
right: 0;
bottom: 0;
padding: 0 5px;
font-size: 14px;
color: var(--keyColor);
background-color: transparent;
.nameEdit {
font-weight: 700;
font-size: 1.6rem;
margin-bottom: 6px;
margin-right: 6px;
flex-shrink: unset;
background: transparent;
border: 0px;
cursor: pointer;
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
justify-content: flex-end;
align-items: flex-end;
font-weight: 600;
color: inherit;
font-family: inherit;
text-transform: uppercase;
}
}
.playlist-desc-expanded {
box-sizing: border-box;
font-size: 14px;
position: relative;
.playlist-artist {
font-size: 20px;
margin-bottom: 6px;
margin-right: 6px;
flex-shrink: unset;
}
.more-btn {
appearance: none;
position: absolute;
right: 0;
bottom: 0;
padding: 0 5px;
.playlist-desc {
box-sizing: border-box;
font-size: 14px;
color: var(--keyColor);
background-color: transparent;
border: 0px;
cursor: pointer;
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
justify-content: flex-end;
align-items: flex-end;
font-weight: 600;
font-family: inherit;
text-transform: uppercase;
flex-shrink: unset;
margin-right: 5px;
max-height: 100px;
position: relative;
.content {
height: 100px;
-webkit-mask-image: -webkit-gradient(linear, left 50%, left 90%, from(rgba(0, 0, 0, 1)), to(rgba(0, 0, 0, 0)));
}
.more-btn {
appearance: none;
position: absolute;
right: 0;
bottom: 0;
padding: 0 5px;
font-size: 14px;
color: var(--keyColor);
background-color: transparent;
border: 0px;
cursor: pointer;
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
justify-content: flex-end;
align-items: flex-end;
font-weight: 600;
font-family: inherit;
text-transform: uppercase;
}
}
.playlist-desc-expanded {
box-sizing: border-box;
font-size: 14px;
position: relative;
.more-btn {
appearance: none;
position: absolute;
right: 0;
bottom: 0;
padding: 0 5px;
font-size: 14px;
color: var(--keyColor);
background-color: transparent;
border: 0px;
cursor: pointer;
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
justify-content: flex-end;
align-items: flex-end;
font-weight: 600;
font-family: inherit;
text-transform: uppercase;
}
}
}
}
}
.friends-info {
@ -2539,26 +2688,6 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb {
}
}
.playlist-more {
border-radius: 100%;
background: var(--keyColor);
box-shadow: var(--ciderShadow-Generic);
width: 36px;
height: 36px;
float: right;
border: 0px;
cursor: pointer;
z-index: 5;
&:hover {
background: var(--keyColor-rollover);
}
&:active {
background: var(--keyColor-pressed);
}
}
.playlist-time {
font-size: 0.9em;
margin: 6px;
@ -2566,6 +2695,14 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb {
}
}
@keyframes playlistArtworkFadeIn {
0%{
opacity: 0;
}
100%{
opacity: 0.7;
}
}
// Collection Page
.collection-page {
padding-bottom: 128px;
@ -2608,8 +2745,21 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb {
padding: 0px;
top: 0;
.floating-header {
position: sticky;
top: 0;
left: 0;
border-bottom: 1px solid rgba(200, 200, 200, 0.05);
z-index: 6;
padding: 0px 1em;
backdrop-filter: blur(32px);
background: rgba(24, 24, 24, 0.15);
top: var(--navigationBarHeight);
transition: opacity 0.1s var(--appleEase);
}
.artist-header {
background: linear-gradient(45deg, var(--keyColor), #0e0e0e);
//background: linear-gradient(45deg, var(--keyColor), #0e0e0e);
color: white;
display: flex;
align-items: center;
@ -2617,26 +2767,36 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb {
min-height: 400px;
position: relative;
.artist-more {
border-radius: 100%;
background: var(--keyColor);
box-shadow: var(--ciderShadow-Generic);
width: 36px;
height: 36px;
.header-content {
z-index: 1;
}
.artworkContainer {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
margin: 0;
padding: 0;
-webkit-mask-image: radial-gradient(at top left, black, transparent 70%), radial-gradient(at top right, black, transparent 70%), linear-gradient(180deg, rgb(200 200 200), transparent 98%);
opacity: .7;
animation: playlistArtworkFadeIn 1s var(--appleEase);
.artworkMaterial>img {
filter: brightness(100%) blur(80px) saturate(100%) contrast(1);
object-position: center;
object-fit: cover;
width: 100%;
height: 100%;
transform: unset;
}
}
.more-btn-round {
position: absolute;
bottom: 26px;
right: 32px;
border: 0px;
cursor: pointer;
z-index: 5;
&:hover {
background: var(--keyColor-rollover);
}
&:active {
background: var(--keyColor-pressed);
}
}
.animated {
@ -2703,28 +2863,31 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb {
}
}
.artist-play {
width: 36px;
height: 36px;
background: var(--keyColor);
border-radius: 100%;
box-shadow: var(--mediaItemShadow);
display: none;
cursor: pointer;
appearance: none;
border: 0px;
padding: 0px;
&:hover {
background: var(--keyColor-rollover);
}
&:active {
background: var(--keyColor-pressed);
}
}
.artist-title {
.artist-play {
width: 36px;
height: 36px;
background: var(--keyColor);
border-radius: 100%;
margin: 14px;
box-shadow: var(--mediaItemShadow);
display: none;
cursor: pointer;
appearance: none;
border: 0px;
padding: 0px;
transform: translateY(3px);
&:hover {
background: var(--keyColor-rollover);
}
&:active {
background: var(--keyColor-pressed);
}
margin: 14px;
}
&.artist-animation-on {
@ -2742,7 +2905,8 @@ input[type="range"].web-slider.display--small::-webkit-slider-thumb {
}
.artist-body {
padding: var(--contentInnerPadding);
padding: 0px var(--contentInnerPadding) 0px var(--contentInnerPadding);
margin-top: -48px;
}
.showmoreless {

View file

@ -0,0 +1,37 @@
<script type="text/x-template" id="artwork-material">
<div class="artworkMaterial">
<img :src="src" v-for="image in images"/>
</div>
</script>
<script>
Vue.component('artwork-material', {
template: '#artwork-material',
data: function () {
return {
src: ""
}
},
mounted() {
this.src = app.getMediaItemArtwork(this.url, this.size)
},
props: {
url: {
type: String,
required: true
},
size: {
type: [String, Number],
required: false,
default: '32'
},
images: {
type: [String, Number],
required: false,
default: '2'
}
},
methods: {
}
});
</script>

View file

@ -70,14 +70,14 @@
},
async select(e) {
let u = this.item
let u1 = await app.mk.api.library.artistRelationship(u.id,"albums",
let u1 = await app.mk.api.v3.music(`/v1/me/library/artists/${u.id}/albums`,
{platform: "web",
"include[library-albums]": "artists,tracks",
"include[library-artists]": "catalog",
"fields[artists]": "url",
"includeOnly": "catalog,artists"}
)
app.showCollection({data : Object.assign({},u1)}, u.attributes.name?? '', '');
app.showCollection({data : Object.assign({},u1.data.data)}, u.attributes.name?? '', '');
},
getArtwork(){
let u = ""

View file

@ -502,22 +502,21 @@
app.mk.setQueue({[truekind]: [item.attributes.playParams.id ?? item.id]}).then(function () {
app.mk.play().then(function (){
var playlistId = id
function getPlaylist(id, params, isLibrary){
function getPlaylist(id, isLibrary){
if (isLibrary){
return app.mk.api.library.playlist(id, params)
} else { return app.mk.api.playlist(id, params)}
return this.app.mk.api.v3.music(`/v1/me/library/playlists/${id}`)
} else { return this.app.mk.api.v3.music(`/v1/catalog/${app.mk.storefrontId}/playlists/${id}`)}
}
try {
getPlaylist(id, params, isLibrary).then(res => {
getPlaylist(id, isLibrary).then(res => {
//let query = res.relationships.tracks.data.map(item => new MusicKit.MediaItem(item));
//if (app.mk.shuffleMode == 1){shuffleArray(query); }
// console.log(query)
// app.mk.queue.append(query)
if (!res.relationships.tracks.next) {
if (!res.data.relationships.tracks.next) {
return
} else {
getPlaylistTracks(res.relationships.tracks.next)
getPlaylistTracks(res.data.relationships.tracks.next)
}
function getPlaylistTracks(next) {

View file

@ -1,6 +1,7 @@
<script type="text/x-template" id="mediaitem-scroller-horizontal">
<template>
<div class="cd-hmedia-scroller" :class="kind">
<slot></slot>
<mediaitem-square :kind="kind" :item="item"
v-for="item in items"></mediaitem-square>
</div>
@ -13,7 +14,7 @@
props: {
'items': {
type: Array,
required: true
required: false
},
'kind': {
type: String,

View file

@ -122,7 +122,11 @@
}
let kind = this.item.attributes.playParams.kind ?? this.item.type ?? '';
var truekind = (!kind.endsWith("s")) ? (kind + "s") : kind;
app.mk.api.library.remove({[truekind]: id})
app.mk.api.v3.music(`v1/me/library/${truekind}/${id.toString()}`,{},
{
fetchOptions: {
method: "DELETE"
}})
this.addedToLibrary = true
},
async contextMenu(event) {

View file

@ -96,6 +96,7 @@
if (this.item.type && !this.item.type.includes("library")) {
var params = {"fields[playlists]": "inLibrary", "fields[albums]": "inLibrary", "relate": "library", "extend": this.revisedRandId()}
var res = await app.mkapi(this.item.attributes.playParams.kind ?? this.item.type, this.item.attributes.playParams.isLibrary ?? false, this.item.attributes.playParams.id ?? this.item.id, params);
res = res.data.data[0]
this.addedToLibrary = (res && res.attributes && res.attributes.inLibrary) ? res.attributes.inLibrary : false
} else {
this.addedToLibrary = true
@ -105,12 +106,17 @@
var params = {"fields[playlists]": "inLibrary","fields[songs]": "inLibrary", "fields[albums]": "inLibrary", "relate": "library", "extend": this.revisedRandId()}
var id = this.item.id ?? this.item.attributes.playParams.id
var res = await app.mkapi(this.item.attributes.playParams.kind ?? this.item.type, this.item.attributes.playParams.isLibrary ?? false, this.item.attributes.playParams.id ?? this.item.id, params);
res = res.data.data[0]
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.item.type ?? '';
var truekind = (!kind.endsWith("s")) ? (kind + "s") : kind;
app.mk.api.library.remove({[truekind]: id})
app.mk.api.v3.music(`v1/me/library/${truekind}/${id.toString()}`,{},
{
fetchOptions: {
method: "DELETE"
}})
this.addedToLibrary = true
},
subtitleSearchNavigate(item) {

View file

@ -145,8 +145,8 @@
let friends = this.badges[id]
if (friends) {
friends.forEach(function (friend) {
self.app.mk.api.socialProfile(friend).then(data => {
self.itemBadges.push(data)
self.app.mk.api.v3.music(`/v1/social/${app.mk.storefrontId}/social-profiles/${friend}`).then(data => {
self.itemBadges.push(data.data.data[0])
})
})
}
@ -164,6 +164,7 @@
"extend": this.revisedRandId()
}
var res = await app.mkapi(this.item.attributes.playParams.kind ?? this.item.type, this.item.attributes.playParams.isLibrary ?? false, this.item.attributes.playParams.id ?? this.item.id, params);
res = res.data.data[0]
this.addedToLibrary = (res && res.attributes && res.attributes.inLibrary) ? res.attributes.inLibrary : false
} else {
this.addedToLibrary = true
@ -179,12 +180,17 @@
}
var id = this.item.id ?? this.item.attributes.playParams.id
var res = await app.mkapi(this.item.attributes.playParams.kind ?? this.item.type, this.item.attributes.playParams.isLibrary ?? false, this.item.attributes.playParams.id ?? this.item.id, params);
res= res.data.data[0]
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.item.type ?? '';
var truekind = (!kind.endsWith("s")) ? (kind + "s") : kind;
app.mk.api.library.remove({[truekind]: id})
app.mk.api.v3.music(`v1/me/library/${truekind}/${id.toString()}`,{},
{
fetchOptions: {
method: "DELETE"
}})
this.addedToLibrary = true
},
uuidv4() {

View file

@ -181,7 +181,9 @@
this.children = []
this.getChildren()
this.toggleFolder()
this.$root.mk.api.library.playlistFolderChildren(item.id).then(children => {
this.$root.mk.api.v3.music(`v1/me/library/playlist-folders/${item.id}/children`).then(data => {
let children = data.data.data;
children.forEach(child => {
if(!self.$root.playlists.listing.find(listing => listing.id == child.id)) {
child.parent = self.item.id

View file

@ -115,11 +115,11 @@
</div>
</div>
<template v-if="mk.nowPlayingItem['attributes']['playParams']">
<div class="actions"
v-if="isInLibrary(mk.nowPlayingItem['attributes']['playParams'])">
❤️
<div class="actions">
<button class="lcdMenu" @click="nowPlayingContextMenu">
<div class="svg-icon"></div>
</button>
</div>
<div class="actions" v-else>🖤</div>
</template>
</div>
@ -661,6 +661,8 @@
</button>
</script>
<!-- Artwork Material -->
<%- include('components/artwork-material') %>
<!-- Menu Panel -->
<%- include('components/menu-panel') %>
<!-- Playlist Listing -->
@ -712,5 +714,6 @@
<script src="index.js?v=1"></script>
<script src="https://cdn.jsdelivr.net/npm/resonance-audio/build/resonance-audio.min.js"></script>
<script src="/audio/audio.js?v=1"></script>
<script src="/WSAPI_Interop.js"></script>
</body>
</html>

View file

@ -1,5 +1,30 @@
<script type="text/x-template" id="cider-artist-feed">
<div class="content-inner">
<div>
<div class="row">
<div class="col">
<div class="row nopadding">
<div class="col nopadding">
<h3>Followed Artists</h3>
</div>
</div>
<div class="well">
<mediaitem-scroller-horizontal>
<div v-for="artist in artists" style="margin: 6px;">
<mediaitem-square :item="artist" kind="small"></mediaitem-square>
<button @click="unfollow(artist.id)" class="md-btn md-btn-glyph" style="display:flex;">
<div class="sidebar-icon">
<div class="svg-icon" :style="{'--url': 'url(./assets/feather/x-circle.svg)'}"></div>
</div> Unfollow
</button>
</div>
</mediaitem-scroller-horizontal>
</div>
</div>
</div>
</div>
<div>
<div class="row">
<div class="col">
@ -30,6 +55,7 @@
app: this.$root,
followedArtists: this.$root.cfg.home.followedArtists,
artistFeed: [],
artists: []
}
},
async mounted() {
@ -37,19 +63,26 @@
await this.getArtistFeed()
},
methods: {
unfollow(id) {
let index = this.followedArtists.indexOf(id)
if (index > -1) {
this.followedArtists.splice(index, 1)
}
let artist = this.artists.find(a => a.id == id)
let index2 = this.artists.indexOf(artist)
if (index2 > -1) {
this.artists.splice(index2, 1)
}
this.getArtistFeed()
},
async getArtistFeed() {
let artists = this.followedArtists
let self = this
this.app.mk.api.artists(artists, {
"views": "featured-release,full-albums,appears-on-albums,featured-albums,featured-on-albums,singles,compilation-albums,live-albums,latest-release,top-music-videos,similar-artists,top-songs,playlists,more-to-hear,more-to-see",
"extend": "artistBio,bornOrFormed,editorialArtwork,editorialVideo,isGroup,origin,hero",
"extend[playlists]": "trackCount",
"include[songs]": "albums",
"fields[albums]": "artistName,artistUrl,artwork,contentRating,editorialArtwork,editorialVideo,name,playParams,releaseDate,url,trackCount",
"limit[artists:top-songs]": 20,
"art[url]": "f"
}, {includeResponseMeta: !0}).then(artistData => {
artistData.data.forEach(item => {
this.artists = []
this.artistFeed = []
this.app.mk.api.v3.music(`/v1/catalog/${app.mk.storefrontId}/artists?ids=${artists.toString()}&views=latest-release&include[songs]=albums&fields[albums]=artistName,artistUrl,artwork,contentRating,editorialArtwork,editorialVideo,name,playParams,releaseDate,url,trackCount&limit[artists:top-songs]=2&art[url]=f`).then(artistData => {
artistData.data.data.forEach(item => {
self.artists.push(item)
if (item.views["latest-release"].data.length != 0) {
self.artistFeed.push(item.views["latest-release"].data[0])
}

View file

@ -1,42 +1,59 @@
<script type="text/x-template" id="cider-artist">
<div class="content-inner artist-page">
<div class="artist-header" :style="getArtistPalette(data)" :key="data.id">
<div class="artist-header" :key="data.id" v-observe-visibility="{callback: isHeaderVisible}">
<animatedartwork-view
:priority="true"
v-if="data.attributes.editorialVideo && (data.attributes.editorialVideo.motionArtistWide16x9 || data.attributes.editorialVideo.motionArtistFullscreen16x9)"
:video="data.attributes.editorialVideo.motionArtistWide16x9.video ?? (data.attributes.editorialVideo.motionArtistFullscreen16x9.video ?? '')">
</animatedartwork-view>
<div class="row">
<div class="col-sm" style="width: auto;">
<div class="artist-image" v-if="!(data.attributes.editorialVideo && (data.attributes.editorialVideo.motionArtistWide16x9 || data.attributes.editorialVideo.motionArtistFullscreen16x9))">
<mediaitem-artwork
shadow="large"
:url="data.attributes.artwork ? data.attributes.artwork.url : ''"
size="190" type="artists"></mediaitem-artwork>
<button class="overlay-play" @click="app.mk.setStationQueue({artist:'a-'+data.id}).then(()=>{
<div class="header-content">
<div class="row">
<div class="col-sm" style="width: auto;">
<div class="artist-image" v-if="!(data.attributes.editorialVideo && (data.attributes.editorialVideo.motionArtistWide16x9 || data.attributes.editorialVideo.motionArtistFullscreen16x9))">
<mediaitem-artwork
shadow="large"
:url="data.attributes.artwork ? data.attributes.artwork.url : ''"
size="190" type="artists"></mediaitem-artwork>
<button class="overlay-play" @click="app.mk.setStationQueue({artist:'a-'+data.id}).then(()=>{
app.mk.play()
})">
<%- include("../svg/play.svg") %>
</button>
<%- include("../svg/play.svg") %>
</button>
</div>
</div>
</div>
<div class="col flex-center artist-title"
:class="{'artist-animation-on': (data.attributes.editorialVideo && (data.attributes.editorialVideo.motionArtistWide16x9 || data.attributes.editorialVideo.motionArtistFullscreen16x9)) }"
>
<button class="artist-play" @click="app.mk.setStationQueue({artist:'a-'+data.id}).then(()=>{
<div class="col flex-center artist-title"
:class="{'artist-animation-on': (data.attributes.editorialVideo && (data.attributes.editorialVideo.motionArtistWide16x9 || data.attributes.editorialVideo.motionArtistFullscreen16x9)) }"
>
<button class="artist-play" @click="app.mk.setStationQueue({artist:'a-'+data.id}).then(()=>{
app.mk.play()
})"><%- include("../svg/play.svg") %></button>
<h1>{{ data.attributes.name }}</h1>
<h1>{{ data.attributes.name }}</h1>
</div>
</div>
<button class="more-btn-round" @click="artistMenu">
<div class="svg-icon"></div>
</button>
</div>
<div class="artworkContainer" v-if="!(data.attributes.editorialVideo && (data.attributes.editorialVideo.motionArtistWide16x9 || data.attributes.editorialVideo.motionArtistFullscreen16x9))">
<artwork-material :url="data.attributes.artwork.url" size="190" images="1"></artwork-material>
</div>
</div>
<div class="floating-header" :style="{opacity: (headerVisible ? 0 : 1),'pointer-events': (headerVisible ? 'none' : '')}">
<div class="row">
<div class="col-auto flex-center">
<button class="artist-play" style="display:block;" @click="app.mk.setStationQueue({artist:'a-'+data.id}).then(()=>{
app.mk.play()
})"><%- include("../svg/play.svg") %></button>
</div>
<div class="col">
<h3>{{ data.attributes.name }}</h3>
</div>
<div class="col-auto flex-center">
<button class="more-btn-round" @click="artistMenu">
<div class="svg-icon"></div>
</button>
</div>
</div>
<button class="artist-more" @click="artistMenu">
<div style=" margin-top: -1px;
margin-left: -6px;
width: 36px;
height: 36px;">
<%- include("../svg/more.svg") %>
</div>
</button>
</div>
<div class="artist-body">
<div class="row well">
@ -133,10 +150,14 @@
data: function () {
return {
topSongsExpanded: false,
app: this.$root
app: this.$root,
headerVisible: true
}
},
methods: {
isHeaderVisible(visible) {
this.headerVisible = visible
},
artistMenu (event) {
let self = this
let followAction = "follow"

View file

@ -7,81 +7,118 @@
</div>
</template>
<template v-if="app.playlists.loadingState == 1">
<div class="playlist-display row"
<div class="playlist-display"
:style="{
background: (data.attributes.artwork != null && data.attributes.artwork['bgColor'] != null) ? ('#' + data.attributes.artwork.bgColor) : '',
color: (data.attributes.artwork != null && data.attributes.artwork['textColor1'] != null) ? ('#' + data.attributes.artwork.textColor1) : ''
'--bgColor': (data.attributes.artwork != null && data.attributes.artwork['bgColor'] != null) ? ('#' + data.attributes.artwork.bgColor) : '',
'--textColor': (data.attributes.artwork != null && data.attributes.artwork['textColor1'] != null) ? ('#' + data.attributes.artwork.textColor1) : ''
}">
<div class="col-auto flex-center">
<div style="width: 260px;height:260px;">
<mediaitem-artwork
:video-priority="true"
:url="(data.attributes != null && data.attributes.artwork != null) ? data.attributes.artwork.url : ((data.relationships != null && data.relationships.tracks.data.length > 0 && data.relationships.tracks.data[0].attributes != null) ? ((data.relationships.tracks.data[0].attributes.artwork != null)? data.relationships.tracks.data[0].attributes.artwork.url : ''):'')"
:video="(data.attributes != null && data.attributes.editorialVideo != null) ? (data.attributes.editorialVideo.motionDetailSquare ? data.attributes.editorialVideo.motionDetailSquare.video : (data.attributes.editorialVideo.motionSquareVideo1x1 ? data.attributes.editorialVideo.motionSquareVideo1x1.video : '')) : '' "
size="260"
></mediaitem-artwork>
</div>
</div>
<div class="col playlist-info">
<template v-if="!editorialNotesExpanded">
<div>
<div class="playlist-name" @click="editPlaylistName()" v-show="!nameEditing">
{{data.attributes ? (data.attributes.name ??
(data.attributes.title ?? '') ?? '') : ''}}
<div class="playlistInfo">
<div class="row">
<div class="col-auto flex-center">
<div style="width: 260px;height:260px;">
<mediaitem-artwork
shadow="large"
:video-priority="true"
:url="(data.attributes != null && data.attributes.artwork != null) ? data.attributes.artwork.url : ((data.relationships != null && data.relationships.tracks.data.length > 0 && data.relationships.tracks.data[0].attributes != null) ? ((data.relationships.tracks.data[0].attributes.artwork != null)? data.relationships.tracks.data[0].attributes.artwork.url : ''):'')"
:video="(data.attributes != null && data.attributes.editorialVideo != null) ? (data.attributes.editorialVideo.motionDetailSquare ? data.attributes.editorialVideo.motionDetailSquare.video : (data.attributes.editorialVideo.motionSquareVideo1x1 ? data.attributes.editorialVideo.motionSquareVideo1x1.video : '')) : '' "
size="260"
></mediaitem-artwork>
</div>
<div class="playlist-name" v-show="nameEditing"><input type="text" spellcheck="false"
class="nameEdit"
v-model="data.attributes.name"
@blur="editPlaylist"
@change="editPlaylist"
@keydown.enter="editPlaylist"/></div>
<div class="playlist-artist item-navigate"
v-if="getArtistName(data) != ''"
@click="data.attributes && data.attributes.artistName ? app.searchAndNavigate(data,'artist') : ''">
{{getArtistName(data)}}
</div>
<div class="playlist-desc" v-if="data.attributes.description && (data.attributes.description.standard || data.attributes.description.short)">
<div v-if="data.attributes.description.short" class="content" v-html="data.attributes.description.short"></div>
<div v-else-if="data.attributes.description.standard" class="content" v-html="data.attributes.description.standard"></div>
<button v-if="data.attributes.description.short" class="more-btn"
@click="editorialNotesExpanded = !editorialNotesExpanded">
More
</div>
<div class="col playlist-info">
<template v-if="!editorialNotesExpanded">
<div>
<div class="playlist-name" @click="editPlaylistName()" v-show="!nameEditing">
{{data.attributes ? (data.attributes.name ??
(data.attributes.title ?? '') ?? '') : ''}}
</div>
<div class="playlist-name" v-show="nameEditing"><input type="text" spellcheck="false"
class="nameEdit"
v-model="data.attributes.name"
@blur="editPlaylist"
@change="editPlaylist"
@keydown.enter="editPlaylist"/></div>
<div class="playlist-artist item-navigate"
v-if="getArtistName(data) != ''"
@click="data.attributes && data.attributes.artistName ? app.searchAndNavigate(data,'artist') : ''">
{{getArtistName(data)}}
</div>
<div class="playlist-desc" v-if="data.attributes.description && (data.attributes.description.standard || data.attributes.description.short)">
<div v-if="data.attributes.description.short" class="content" v-html="data.attributes.description.short"></div>
<div v-else-if="data.attributes.description.standard" class="content" v-html="data.attributes.description.standard"></div>
<button v-if="data.attributes.description.short" class="more-btn"
@click="editorialNotesExpanded = !editorialNotesExpanded">
More
</button>
</div>
</div>
</template>
<template v-if="editorialNotesExpanded">
<div class="playlist-desc-expanded">
<div class="content"
v-html="((data.attributes.editorialNotes) ? (data.attributes.editorialNotes.standard ?? (data.attributes.editorialNotes.short ?? '') ) : (data.attributes.description ? (data.attributes.description.standard ?? (data.attributes.description.short ?? '')) : ''))"></div>
<button class="more-btn" @click="editorialNotesExpanded = !editorialNotesExpanded">Less
</button>
</div>
</template>
<div class="playlist-controls" v-observe-visibility="{callback: isHeaderVisible}">
<button class="md-btn" style="min-width: 120px;"
@click="app.mk.shuffleMode = 0; play()">
Play
</button>
<button class="md-btn" style="min-width: 120px;"
@click="app.mk.shuffleMode = 1;play()">
Shuffle
</button>
<button class="md-btn" style="min-width: 120px;" v-if="inLibrary!=null && confirm!=true"
@click="confirmButton()">
{{ (!inLibrary) ? "Add to Library" : "Remove from Library" }}
</button>
<button class="md-btn" style="min-width: 120px;" v-if="confirm==true"
@click="(!inLibrary) ? addToLibrary(data.attributes.playParams.id.toString()) : removeFromLibrary(data.attributes.playParams.id.toString()) ">
Confirm?
</button>
<button class="more-btn-round" style="float:right;" @click="menu">
<div class="svg-icon"></div>
</button>
</div>
</div>
</template>
<template v-if="editorialNotesExpanded">
<div class="playlist-desc-expanded">
<div class="content"
v-html="((data.attributes.editorialNotes) ? (data.attributes.editorialNotes.standard ?? (data.attributes.editorialNotes.short ?? '') ) : (data.attributes.description ? (data.attributes.description.standard ?? (data.attributes.description.short ?? '')) : ''))"></div>
<button class="more-btn" @click="editorialNotesExpanded = !editorialNotesExpanded">Less
</div>
</div>
<div class="artworkContainer" v-if="data.attributes.artwork != null">
<artwork-material :url="data.attributes.artwork.url" size="260" images="1"></artwork-material>
</div>
</div>
<div class="floating-header" :style="{opacity: (headerVisible ? 0 : 1),'pointer-events': (headerVisible ? 'none' : '')}">
<div class="row">
<div class="col">
<h3>{{data.attributes ? (data.attributes.name ??
(data.attributes.title ?? '') ?? '') : ''}}</h3>
</div>
<div class="col-auto flex-center">
<div>
<button class="md-btn" style="min-width: 120px;"
@click="app.mk.shuffleMode = 0; play()">
Play
</button>
<button class="md-btn" style="min-width: 120px;"
@click="app.mk.shuffleMode = 1;play()">
Shuffle
</button>
<button class="md-btn" style="min-width: 120px;" v-if="inLibrary!=null && confirm!=true"
@click="confirmButton()">
{{ (!inLibrary) ? "Add to Library" : "Remove from Library" }}
</button>
<button class="md-btn" style="min-width: 120px;" v-if="confirm==true"
@click="(!inLibrary) ? addToLibrary(data.attributes.playParams.id.toString()) : removeFromLibrary(data.attributes.playParams.id.toString()) ">
Confirm?
</button>
</div>
</template>
<div class="playlist-controls">
<button class="md-btn" style="min-width: 120px;"
@click="app.mk.shuffleMode = 0; play()">
Play
</button>
<button class="md-btn" style="min-width: 120px;"
@click="app.mk.shuffleMode = 1;play()">
Shuffle
</button>
<button class="md-btn" style="min-width: 120px;" v-if="inLibrary!=null && confirm!=true"
@click="confirmButton()">
{{ (!inLibrary) ? "Add to Library" : "Remove from Library" }}
</button>
<button class="md-btn" style="min-width: 120px;" v-if="confirm==true"
@click="(!inLibrary) ? addToLibrary(data.attributes.playParams.id.toString()) : removeFromLibrary(data.attributes.playParams.id.toString()) ">
Confirm?
</button>
<button class="playlist-more" @click="menu">
<div style=" margin-top: -1px;
margin-left: -5px;
width: 36px;
height: 36px;">
<%- include("../svg/more.svg") %>
</div>
</div>
<div class="col-auto flex-center">
<button class="more-btn-round" style="float:right;" @click="menu">
<div class="svg-icon"></div>
</button>
</div>
</div>
@ -118,6 +155,20 @@
style="width: 50%;">
{{data.attributes.copyright}}
</div>
<hr>
<template v-if="typeof data.meta != 'undefined'">
<div v-for="view in data.meta.views.order" v-if="data.views[view].data.length != 0">
<div class="row" >
<div class="col">
<h3>{{ data.views[view].attributes.title }}</h3>
</div>
</div>
<div>
<mediaitem-scroller-horizontal :items="data.views[view].data"></mediaitem-scroller-horizontal>
</div>
</div>
</template>
</div>
</template>
</div>
@ -138,7 +189,8 @@
confirm: false,
app: this.$root,
itemBadges: [],
badgesRequested: false
badgesRequested: false,
headerVisible: true
}
},
mounted: function () {
@ -153,6 +205,9 @@
}
},
methods: {
isHeaderVisible(visible) {
this.headerVisible = visible
},
getBadges() {
return
if (this.badgesRequested) {
@ -171,8 +226,8 @@
let friends = badges[id]
if (friends) {
friends.forEach(function (friend) {
self.app.mk.api.socialProfile(friend).then(data => {
self.itemBadges.push(data)
self.app.mk.api.v3.music(`/v1/social/${app.mk.storefrontId}/social-profiles/${friend}`).then(data => {
self.itemBadges.push(data.data.data[0])
})
})
}
@ -205,7 +260,7 @@
"relate": "library"
};
const res = await app.mkapi(this.data.attributes.playParams.kind ?? this.data.type, this.data.attributes.playParams.isLibrary ?? false, this.data.attributes.playParams.id ?? this.data.id, params);
this.inLibrary = (res && res.attributes && res.attributes.inLibrary) ? res.attributes.inLibrary : false
this.inLibrary = (res.data.data[0] && res.data.data[0].attributes && res.data.data[0].attributes.inLibrary) ? res.data.data[0].attributes.inLibrary : false
console.log(res)
} else {
this.inLibrary = true
@ -229,12 +284,16 @@
const params = {"fields[somgs]": "inLibrary", "fields[albums]": "inLibrary", "relate": "library"};
var id = this.data.id ?? this.data.attributes.playParams.id
const res = await app.mkapi(this.data.attributes.playParams.kind ?? this.data.type, this.data.attributes.playParams.isLibrary ?? false, this.data.attributes.playParams.id ?? this.data.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
if (res.data.data[0] && res.data.data[0].relationships && res.data.data[0].relationships.library && res.data.data[0].relationships.library.data && res.data.data[0].relationships.library.data.length > 0) {
id = res.data.data[0].relationships.library.data[0].id
}
let kind = this.data.attributes.playParams.kind ?? this.data.type ?? '';
const truekind = (!kind.endsWith("s")) ? (kind + "s") : kind;
app.mk.api.library.remove({[truekind]: id})
app.mk.api.v3.music(`v1/me/library/${truekind}/${id.toString()}`,{},
{
fetchOptions: {
method: "DELETE"
}})
this.inLibrary = false
this.confirm = false
},
@ -274,7 +333,19 @@
if (!this.data.attributes.canEdit) {
return
}
await app.mk.api.library.putPlaylistTracklisting(this.data.attributes.playParams.id, this.convert())
console.log('sds',this.convert())
await app.mk.api.v3.music(
`/v1/me/library/playlists/${this.data.attributes.playParams.id}/tracks`,
{},
{
fetchOptions: {
method: "PUT",
body: JSON.stringify({
data: this.convert()
})
}
}
)
},
async remove() {
if (!this.data.attributes.canEdit) {

View file

@ -14,13 +14,16 @@
<mediaitem-square v-else :item="item" :type="getKind(item)"></mediaitem-square>
</template>
</template>
<button v-if="triggerEnabled" style="opacity:0;height: 32px;" v-observe-visibility="{callback: visibilityChanged}">Show More</button>
<button v-if="triggerEnabled" style="opacity:0;height: 32px;"
v-observe-visibility="{callback: visibilityChanged}">Show More
</button>
</div>
<transition name="fabfade">
<button class="top-fab" v-show="showFab" @click="scrollToTop()">
<%- include("../svg/arrow-up.svg") %>
</button>
</transition>
<div class="well" v-show="loading"><div class="spinner"></div></div>
</div>
</script>
<script>
@ -47,16 +50,17 @@
canSeeTrigger: false,
showFab: false,
commonKind: "song",
api: this.$root.mk.api
api: this.$root.mk.api,
loading: false
}
},
methods: {
getKind(item) {
if(typeof item.kind != "undefined") {
if (typeof item.kind != "undefined") {
this.commonKind = item.kind;
return item.kind
}
if(typeof item.attributes.playParams != "undefined") {
if (typeof item.attributes.playParams != "undefined") {
this.commonKind = item.attributes.playParams.kind
return item.attributes.playParams.kind
}
@ -71,73 +75,48 @@
})
},
getNext() {
// if this.data.next is not null, then we can run this.data.next() and concat to this.data.data to get the next page
switch(this.type) {
default:
case "artists":
if (this.data.next && this.triggerEnabled) {
this.triggerEnabled = false;
let nextFn = (data => {
console.log(data);
this.data.next = data.next;
this.data.data = this.data.data.concat(data.data);
this.triggerEnabled = true;
});
if(typeof this.data.next == "function") {
this.data.next().then(data => nextFn(data));
}else{
this.api.v3.music(this.data.next).then(data => nextFn(data));
}
}else{
console.log("No next page");
this.triggerEnabled = false;
}
break;
case "search":
if (this.data.next && this.triggerEnabled) {
this.triggerEnabled = false;
this.data.next().then(data => {
console.log(data);
this.data.next = data[this.data.groups].next;
this.data.data = this.data.data.concat(data[this.data.groups].data.data);
this.triggerEnabled = true;
});
}else{
console.log("No next page");
this.triggerEnabled = false;
}
break;
case "listen_now":
case "curator":
if (this.data.next && this.triggerEnabled) {
this.triggerEnabled = false;
app.mk.api.v3.music(this.data.next).then(data => {
console.log(data);
this.data.next = data.data.next;
this.data.data = this.data.data.concat(data.data.data);
this.triggerEnabled = true;
});
}else{
console.log("No next page");
this.triggerEnabled = false;
}
break;
let self = this
this.triggerEnabled = false;
if (typeof this.data.next == "undefined") {
return
}
this.loading = true
this.api.v3.music(this.data.next, app.collectionList.requestBody).then((response) => {
console.log(response)
if (!app.collectionList.response.groups) {
if (response.data.next) {
this.data.data = this.data.data.concat(response.data.data);
this.data.next = response.data.next;
this.triggerEnabled = true;
}
this.loading = false
}else{
if(!response.data.results[app.collectionList.response.groups]) {
this.loading = false
return
}
if (response.data.results[app.collectionList.response.groups].next) {
this.data.data = this.data.data.concat(response.data.results[app.collectionList.response.groups].data);
this.data.next = response.data.results[app.collectionList.response.groups].next;
this.triggerEnabled = true;
this.loading = false
}
}
})
},
headerVisibility: function (isVisible, entry) {
if(isVisible) {
if (isVisible) {
this.showFab = false;
}else{
} else {
this.showFab = true;
}
},
visibilityChanged: function (isVisible, entry) {
if(isVisible) {
if (isVisible) {
this.canSeeTrigger = true;
this.getNext();
}else{
} else {
this.canSeeTrigger = false;
}
}

View file

@ -131,32 +131,22 @@
playlists.push(item.id)
}
}
if (playlists.length != 0) {
this.app.mk.api.playlists(playlists).then(playlistsData => {
self.favorites.push(...playlistsData)
if (playlists.length != 0) {
this.app.mk.api.v3.music(`/v1/catalog/${app.mk.storefrontId}/playlists/${playlists.toString()}`).then(playlistsData => {
self.favorites.push(...playlistsData.data)
})
}
if (libraryPlaylists.length != 0) {
this.app.mk.api.library.playlists(libraryPlaylists).then(playlistsData => {
self.favorites.push(...playlistsData)
if (libraryPlaylists.length != 0) {
this.app.mk.api.v3.music(`v1/me/library/playlists/${playlists.toString()}`).then(playlistsData => {
self.favorites.push(...playlistsData.data)
})
}
},
async getArtistFeed() {
let artists = this.followedArtists
let self = this
this.app.mk.api.artists(artists, {
"views": "featured-release,full-albums,appears-on-albums,featured-albums,featured-on-albums,singles,compilation-albums,live-albums,latest-release,top-music-videos,similar-artists,top-songs,playlists,more-to-hear,more-to-see",
"extend": "artistBio,bornOrFormed,editorialArtwork,editorialVideo,isGroup,origin,hero",
"extend[playlists]": "trackCount",
"include[songs]": "albums",
"fields[albums]": "artistName,artistUrl,artwork,contentRating,editorialArtwork,editorialVideo,name,playParams,releaseDate,url,trackCount",
"limit[artists:top-songs]": 20,
"art[url]": "f"
}, {
includeResponseMeta: !0
}).then(artistData => {
artistData.data.forEach(item => {
this.app.mk.api.v3.music(`/v1/catalog/${app.mk.storefrontId}/artists?ids=${artists.toString()}&views=latest-release&include[songs]=albums&fields[albums]=artistName,artistUrl,artwork,contentRating,editorialArtwork,editorialVideo,name,playParams,releaseDate,url,trackCount&limit[artists:top-songs]=2&art[url]=f`).then(artistData => {
artistData.data.data.forEach(item => {
if (item.views["latest-release"].data.length != 0) {
self.artistFeed.push(item.views["latest-release"].data[0])
}
@ -176,35 +166,10 @@
},
async getListenNowData() {
let self = this
this.app.mk.api.personalRecommendations("", {
name: "listen-now",
with: "friendsMix,library,social",
"art[social-profiles:url]": "c",
"art[url]": "c,f",
"omit[resource]": "autos",
"relate[editorial-items]": "contents",
extend: ["editorialCard", "editorialVideo"],
"extend[albums]": ["artistUrl"],
"extend[library-albums]": ["artistUrl", "editorialVideo"],
"extend[playlists]": ["artistNames", "editorialArtwork", "editorialVideo"],
"extend[library-playlists]": ["artistNames", "editorialArtwork", "editorialVideo"],
"extend[social-profiles]": "topGenreNames",
"include[albums]": "artists",
"include[songs]": "artists",
"include[music-videos]": "artists",
"fields[albums]": ["artistName", "artistUrl", "artwork", "contentRating", "editorialArtwork", "editorialVideo", "name", "playParams", "releaseDate", "url"],
"fields[artists]": ["name", "url"],
"extend[stations]": ["airDate", "supportsAirTimeUpdates"],
"meta[stations]": "inflectionPoints",
types: "artists,albums,editorial-items,library-albums,library-playlists,music-movies,music-videos,playlists,stations,uploaded-audios,uploaded-videos,activities,apple-curators,curators,tv-shows,social-profiles,social-upsells",
platform: "web"
}, {
includeResponseMeta: !0,
reload: !0
}).then((data) => {
console.log(data.data[1])
this.app.mk.api.v3.music(`/v1/me/recommendations?timezone=${encodeURIComponent(app.formatTimezoneOffset())}&name=listen-now&with=friendsMix,library,social&art[social-profiles:url]=c&art[url]=c,f&omit[resource]=autos&relate[editorial-items]=contents&extend=editorialCard,editorialVideo&extend[albums]=artistUrl&extend[library-albums]=artistUrl,editorialVideo&extend[playlists]=artistNames,editorialArtwork,editorialVideo&extend[library-playlists]=artistNames,editorialArtwork,editorialVideo&extend[social-profiles]=topGenreNames&include[albums]=artists&include[songs]=artists&include[music-videos]=artists&fields[albums]=artistName,artistUrl,artwork,contentRating,editorialArtwork,editorialVideo,name,playParams,releaseDate,url&fields[artists]=name,url&extend[stations]=airDate,supportsAirTimeUpdates&meta[stations]=inflectionPoints&types=artists,albums,editorial-items,library-albums,library-playlists,music-movies,music-videos,playlists,stations,uploaded-audios,uploaded-videos,activities,apple-curators,curators,tv-shows,social-upsells&platform=web`).then((data) => {
console.log(data.data.data[1])
try {
self.madeForYou = data.data.filter(section => {
self.madeForYou = data.data.data.filter(section => {
if (section.meta.metrics.moduleType == "6") {
return section
};
@ -213,8 +178,8 @@
self.sectionsReady.push("madeForYou")
try {
self.recentlyPlayed = data.data[1].relationships.contents.data
self.friendsListeningTo = data.data.filter(section => {
self.recentlyPlayed = data.data.data[1].relationships.contents.data
self.friendsListeningTo = data.data.data.filter(section => {
if (section.meta.metrics.moduleType == "11") {
return section
};
@ -228,7 +193,7 @@
self.profile = response.data.data[0]
})
}
}
}
});
</script>

View file

@ -1,86 +1,101 @@
<script type="text/x-template" id="cider-search">
<div class="content-inner">
<div class="row">
<div class="col-sm" style="width: auto;" v-if="getTopResult()">
<template>
<h3>Top Result</h3>
<mediaitem-square :item="getTopResult()"></mediaitem-square>
</template>
</div>
<div v-else style="text-align: center">
<h3>No Results</h3>
<p>Try a new search.</p>
</div>
<div class="col" v-if="search.results.song">
<div class="row">
<div class="col">
<h3>Songs</h3>
<div v-if="search != null && search != [] && search.term != ''">
<div class="row">
<div class="col-sm" style="width: auto;" v-if="getTopResult()">
<template>
<h3>Top Result</h3>
<mediaitem-square :item="getTopResult()"></mediaitem-square>
</template>
</div>
<div v-else style="text-align: center">
<h3>No Results</h3>
<p>Try a new search.</p>
</div>
<div class="col" v-if="search.results.song">
<div class="row">
<div class="col">
<h3>Songs</h3>
</div>
<div class="col-auto flex-center"
@click="app.showSearchView(app.search.term, 'song', app.friendlyTypes('song'))"
v-if="search.results.song.data.length >= 6">
<button class="cd-btn-seeall">See All</button>
</div>
</div>
<div class="col-auto flex-center"
@click="app.showSearchView(app.search.term, 'song', app.friendlyTypes('song'))"
v-if="search.results.song.data.length >= 6">
<button class="cd-btn-seeall">See All</button>
<div>
<mediaitem-list-item :item="item" :index="index"
v-for="(item, index) in search.results.song.data.limit(6)"></mediaitem-list-item>
</div>
</div>
<div>
<mediaitem-list-item :item="item" :index="index"
v-for="(item, index) in search.results.song.data.limit(6)"></mediaitem-list-item>
</div>
</div>
</div>
<template v-if="search.results['meta']">
<template
v-for="section in search.results.meta.results.order" v-if="section != 'song' && section != 'top'">
<template v-if="search.results['meta'] != null">
<template
v-for="section in search.results.meta.results.order" v-if="section != 'song' && section != 'top'">
<div class="row">
<div class="col">
<h3>{{ app.friendlyTypes(section) }}</h3>
</div>
<div class="col-auto flex-center" v-if="search.results[section].data.length >= 10">
<button class="cd-btn-seeall"
@click="app.showSearchView(app.search.term, section, app.friendlyTypes(section))">See
All
</button>
</div>
</div>
<template v-if="!app.friendlyTypes(section).includes('Video')">
<mediaitem-scroller-horizontal-large
:items="search.results[section].data.limit(10)"></mediaitem-scroller-horizontal-large>
</template>
<template v-else>
<mediaitem-scroller-horizontal-mvview
:items="search.results[section].data.limit(10)"></mediaitem-scroller-horizontal-mvview>
</template>
</template>
</template>
<template v-if="search.resultsSocial.playlist">
<div class="row">
<div class="col">
<h3>{{ app.friendlyTypes(section) }}</h3>
<h3>Shared Playlists</h3>
</div>
<div class="col-auto flex-center" v-if="search.results[section].data.length >= 10">
<div class="col-auto flex-center" v-if="search.resultsSocial.playlist.data.length >= 10">
<button class="cd-btn-seeall"
@click="app.showSearchView(app.search.term, section, app.friendlyTypes(section))">See
All
@click="app.showCollection(search.resultsSocial.playlist, 'Shared Playlists', 'default')">See All
</button>
</div>
</div>
<template v-if="!app.friendlyTypes(section).includes('Video')">
<mediaitem-scroller-horizontal-large
:items="search.results[section].data.limit(10)"></mediaitem-scroller-horizontal-large>
</template>
<template v-else>
<mediaitem-scroller-horizontal-mvview
:items="search.results[section].data.limit(10)"></mediaitem-scroller-horizontal-mvview>
</template>
<mediaitem-scroller-horizontal-large
:items="search.resultsSocial.playlist.data.limit(10)"></mediaitem-scroller-horizontal-large>
</template>
</template>
<template v-if="search.resultsSocial.playlist">
<div class="row">
<div class="col">
<h3>Shared Playlists</h3>
<template v-if="search.resultsSocial.profile">
<div class="row">
<div class="col">
<h3>People</h3>
</div>
<div class="col-auto flex-center" v-if="search.resultsSocial.profile.data.length >= 10">
<button class="cd-btn-seeall"
@click="app.showCollection(search.resultsSocial.profile, 'People', 'default')">See All
</button>
</div>
</div>
<div class="col-auto flex-center" v-if="search.resultsSocial.playlist.data.data.length >= 10">
<button class="cd-btn-seeall"
@click="app.showCollection(search.resultsSocial.playlist.data, 'Shared Playlists', 'default')">See All
</button>
<mediaitem-scroller-horizontal-large
:items="search.resultsSocial.profile.data.limit(10)"></mediaitem-scroller-horizontal-large>
</template>
</div>
<div v-else>
<div v-if="categoriesReady || getCategories()">
<div>
<div class="col" v-if="categoriesView != null && categoriesView != [] && categoriesView[0].attributes != null && categoriesView[0].attributes.title != null">
<h3>{{categoriesView[0].attributes.title.stringForDisplay ?? ""}}</h3>
</div>
<mediaitem-square :kind="'385'" size="600"
:item="item ? (item.attributes.kind ? item : ((item.relationships && item.relationships.contents ) ? item.relationships.contents.data[0] : item)) : []"
:imagesize="800"
v-for="item in categoriesView[1].relationships.contents.data.filter(item => item.type != 'editorial-items')">
</div>
</div>
<mediaitem-scroller-horizontal-large
:items="search.resultsSocial.playlist.data.data.limit(10)"></mediaitem-scroller-horizontal-large>
</template>
<template v-if="search.resultsSocial.profile">
<div class="row">
<div class="col">
<h3>People</h3>
</div>
<div class="col-auto flex-center" v-if="search.resultsSocial.profile.data.data.length >= 10">
<button class="cd-btn-seeall"
@click="app.showCollection(search.resultsSocial.profile.data, 'People', 'default')">See All
</button>
</div>
</div>
<mediaitem-scroller-horizontal-large
:items="search.resultsSocial.profile.data.data.limit(10)"></mediaitem-scroller-horizontal-large>
</template>
</div>
</div>
</div>
</script>
@ -90,8 +105,10 @@
props: ['search'],
data: function () {
return {
app: this.$root
}
app: this.$root,
categoriesView : [],
categoriesReady : false,
}
},
methods: {
getTopResult() {
@ -100,6 +117,13 @@
} catch( error ) {
return false
}
},
async getCategories() {
if(this.categoriesView != [] && this.categoriesView.length > 0) {this.categoriesReady = true; return await true;} else {
let response = await this.app.mk.api.v3.music(`/v1/recommendations/${this.app.mk.storefrontId}?timezone=${encodeURIComponent(this.app.formatTimezoneOffset())}&name=search-landing&platform=web&extend=editorialArtwork&art%5Burl%5D=f%2Cc&types=editorial-items%2Capple-curators%2Cactivities`);
this.categoriesView = response.data.data;
this.categoriesReady = true;
return await true;}
}
}
})

View file

@ -5,5 +5,8 @@
{{ $store.state.test }}
<div class="spinner"></div>
<button class="md-btn">Cider Button</button>
<div style="position: relative;width: 300px;height: 300px;">
<artwork-material url="https://is3-ssl.mzstatic.com/image/thumb/Music126/v4/13/41/13/1341133b-560f-1aee-461f-c4b32ec049b4/cover.jpg/{w}x{h}bb.jpg"></artwork-material>
</div>
</div>
</template>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 28 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,-11,-10)">
<path d="M39,12.24C39,11.004 37.996,10 36.76,10L13.24,10C12.004,10 11,11.004 11,12.24L11,71.76C11,72.996 12.004,74 13.24,74L36.76,74C37.996,74 39,72.996 39,71.76L39,12.24Z" style="fill:rgb(108,108,108);fill-opacity:0.43;"/>
<g transform="matrix(0.714286,0,0,1,7.14286,0)">
<rect x="18" y="41" width="14" height="2" style="fill:rgb(231,231,231);fill-opacity:0.77;"/>
</g>
<g transform="matrix(0.714286,0,0,1,7.14286,-5)">
<rect x="18" y="41" width="14" height="2" style="fill:rgb(231,231,231);fill-opacity:0.77;"/>
</g>
<g transform="matrix(0.714286,0,0,1,7.14286,5)">
<rect x="18" y="41" width="14" height="2" style="fill:rgb(231,231,231);fill-opacity:0.77;"/>
</g>
<g transform="matrix(0.571429,0,0,0.6,10.7143,10.4)">
<path d="M25,26L32,36L18,36L25,26Z" style="fill:rgb(231,231,231);fill-opacity:0.77;"/>
</g>
<g transform="matrix(0.571429,0,0,-0.6,10.7143,73.6)">
<path d="M25,26L32,36L18,36L25,26Z" style="fill:rgb(231,231,231);fill-opacity:0.77;"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M257.5 445.1l-22.2 22.2c-9.4 9.4-24.6 9.4-33.9 0L7 273c-9.4-9.4-9.4-24.6 0-33.9L201.4 44.7c9.4-9.4 24.6-9.4 33.9 0l22.2 22.2c9.5 9.5 9.3 25-.4 34.3L136.6 216H424c13.3 0 24 10.7 24 24v32c0 13.3-10.7 24-24 24H136.6l120.5 114.8c9.8 9.3 10 24.8.4 34.3z"/></svg>

After

Width:  |  Height:  |  Size: 521 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M11.5 280.6l192 160c20.6 17.2 52.5 2.8 52.5-24.6V96c0-27.4-31.9-41.8-52.5-24.6l-192 160c-15.3 12.8-15.3 36.4 0 49.2zm256 0l192 160c20.6 17.2 52.5 2.8 52.5-24.6V96c0-27.4-31.9-41.8-52.5-24.6l-192 160c-15.3 12.8-15.3 36.4 0 49.2z"/></svg>

After

Width:  |  Height:  |  Size: 500 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M500.5 231.4l-192-160C287.9 54.3 256 68.6 256 96v320c0 27.4 31.9 41.8 52.5 24.6l192-160c15.3-12.8 15.3-36.4 0-49.2zm-256 0l-192-160C31.9 54.3 0 68.6 0 96v320c0 27.4 31.9 41.8 52.5 24.6l192-160c15.3-12.8 15.3-36.4 0-49.2z"/></svg>

After

Width:  |  Height:  |  Size: 493 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" fill="white"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M471.1 96C405 96 353.3 137.3 320 174.6 286.7 137.3 235 96 168.9 96 75.8 96 0 167.8 0 256s75.8 160 168.9 160c66.1 0 117.8-41.3 151.1-78.6 33.3 37.3 85 78.6 151.1 78.6 93.1 0 168.9-71.8 168.9-160S564.2 96 471.1 96zM168.9 320c-40.2 0-72.9-28.7-72.9-64s32.7-64 72.9-64c38.2 0 73.4 36.1 94 64-20.4 27.6-55.9 64-94 64zm302.2 0c-38.2 0-73.4-36.1-94-64 20.4-27.6 55.9-64 94-64 40.2 0 72.9 28.7 72.9 64s-32.7 64-72.9 64z"/></svg>

After

Width:  |  Height:  |  Size: 684 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M80 368H16a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16v-64a16 16 0 0 0-16-16zm0-320H16A16 16 0 0 0 0 64v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16V64a16 16 0 0 0-16-16zm0 160H16a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16v-64a16 16 0 0 0-16-16zm416 176H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-320H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16zm0 160H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16z"/></svg>

After

Width:  |  Height:  |  Size: 831 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M144 479H48c-26.5 0-48-21.5-48-48V79c0-26.5 21.5-48 48-48h96c26.5 0 48 21.5 48 48v352c0 26.5-21.5 48-48 48zm304-48V79c0-26.5-21.5-48-48-48h-96c-26.5 0-48 21.5-48 48v352c0 26.5 21.5 48 48 48h96c26.5 0 48-21.5 48-48z"/></svg>

After

Width:  |  Height:  |  Size: 487 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M424.4 214.7L72.4 6.6C43.8-10.3 0 6.1 0 47.9V464c0 37.5 40.7 60.1 72.4 41.3l352-208c31.4-18.5 31.5-64.1 0-82.6z"/></svg>

After

Width:  |  Height:  |  Size: 384 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M464 32H336c-26.5 0-48 21.5-48 48v128c0 26.5 21.5 48 48 48h80v64c0 35.3-28.7 64-64 64h-8c-13.3 0-24 10.7-24 24v48c0 13.3 10.7 24 24 24h8c88.4 0 160-71.6 160-160V80c0-26.5-21.5-48-48-48zm-288 0H48C21.5 32 0 53.5 0 80v128c0 26.5 21.5 48 48 48h80v64c0 35.3-28.7 64-64 64h-8c-13.3 0-24 10.7-24 24v48c0 13.3 10.7 24 24 24h8c88.4 0 160-71.6 160-160V80c0-26.5-21.5-48-48-48z"/></svg>

After

Width:  |  Height:  |  Size: 640 B

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 60 60" style="enable-background:new 0 0 60 60;" xml:space="preserve">
<g>
<path fill="white" d="M42,12H20.414l7.293-7.293c0.391-0.391,0.391-1.023,0-1.414s-1.023-0.391-1.414,0l-8.999,8.999
c-0.093,0.092-0.166,0.203-0.217,0.326c-0.101,0.244-0.101,0.52,0,0.764c0.051,0.123,0.124,0.234,0.217,0.326l8.999,8.999
C26.488,22.902,26.744,23,27,23s0.512-0.098,0.707-0.293c0.391-0.391,0.391-1.023,0-1.414L20.414,14H42c8.822,0,16,7.178,16,16
c0,4.252-1.668,8.264-4.696,11.295c-0.391,0.391-0.391,1.024,0,1.414c0.195,0.195,0.451,0.293,0.707,0.293s0.512-0.098,0.707-0.293
C58.124,39.3,60,34.786,60,30C60,20.075,51.925,12,42,12z"/>
<path fill="white" d="M35.707,37.293c-0.391-0.391-1.023-0.391-1.414,0s-0.391,1.023,0,1.414L41.586,46H18C9.178,46,2,38.822,2,30
c0-3.783,1.359-7.46,3.828-10.354c0.358-0.421,0.309-1.052-0.111-1.41c-0.419-0.359-1.052-0.31-1.41,0.111
C1.529,21.604,0,25.741,0,30c0,9.925,8.075,18,18,18h23.586l-7.293,7.293c-0.391,0.391-0.391,1.023,0,1.414
C34.488,56.902,34.744,57,35,57s0.512-0.098,0.707-0.293l9-9c0.391-0.391,0.391-1.023,0-1.414L35.707,37.293z"/>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z"/></svg>

After

Width:  |  Height:  |  Size: 618 B

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 230.055 230.055" style="enable-background:new 0 0 230.055 230.055;" xml:space="preserve">
<path fill="white" d="M199.419,124.497c-3.516-3.515-9.213-3.515-12.729,0c-3.515,3.515-3.515,9.213,0,12.728l12.637,12.636h-8.406
c-8.177,0-16.151-2.871-22.453-8.083l-32.346-26.751l32.345-26.751c6.303-5.212,14.277-8.083,22.454-8.083h8.406L186.69,92.83
c-3.515,3.515-3.515,9.213,0,12.728c1.758,1.757,4.061,2.636,6.364,2.636s4.606-0.879,6.364-2.636l28-28
c3.515-3.515,3.515-9.213,0-12.728l-28-28c-3.516-3.515-9.213-3.515-12.729,0c-3.515,3.515-3.515,9.213,0,12.728l12.637,12.636
h-8.406c-12.354,0-24.403,4.337-33.926,12.211L122,103.348L82.564,70.733c-6.658-5.507-15.084-8.54-23.724-8.54H9
c-4.971,0-9,4.029-9,9s4.029,9,9,9h49.841c4.462,0,8.813,1.566,12.252,4.411l36.786,30.423L71.094,145.45
c-3.439,2.844-7.791,4.411-12.253,4.411H9c-4.971,0-9,4.029-9,9s4.029,9,9,9h49.841c8.64,0,17.065-3.033,23.725-8.54L122,126.707
l34.996,28.943c9.521,7.875,21.57,12.211,33.925,12.211h8.406l-12.637,12.636c-3.515,3.515-3.515,9.213,0,12.728
c1.758,1.757,4.061,2.636,6.364,2.636s4.606-0.879,6.364-2.636l28-28c3.515-3.515,3.515-9.213,0-12.728L199.419,124.497z"/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M215.03 72.04L126.06 161H24c-13.26 0-24 10.74-24 24v144c0 13.25 10.74 24 24 24h102.06l88.97 88.95c15.03 15.03 40.97 4.47 40.97-16.97V89.02c0-21.47-25.96-31.98-40.97-16.98zm123.2 108.08c-11.58-6.33-26.19-2.16-32.61 9.45-6.39 11.61-2.16 26.2 9.45 32.61C327.98 229.28 336 242.62 336 257c0 14.38-8.02 27.72-20.92 34.81-11.61 6.41-15.84 21-9.45 32.61 6.43 11.66 21.05 15.8 32.61 9.45 28.23-15.55 45.77-45 45.77-76.88s-17.54-61.32-45.78-76.87z"/></svg>

After

Width:  |  Height:  |  Size: 710 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="white" d="M215.03 71.05L126.06 160H24c-13.26 0-24 10.74-24 24v144c0 13.25 10.74 24 24 24h102.06l88.97 88.95c15.03 15.03 40.97 4.47 40.97-16.97V88.02c0-21.46-25.96-31.98-40.97-16.97zm233.32-51.08c-11.17-7.33-26.18-4.24-33.51 6.95-7.34 11.17-4.22 26.18 6.95 33.51 66.27 43.49 105.82 116.6 105.82 195.58 0 78.98-39.55 152.09-105.82 195.58-11.17 7.32-14.29 22.34-6.95 33.5 7.04 10.71 21.93 14.56 33.51 6.95C528.27 439.58 576 351.33 576 256S528.27 72.43 448.35 19.97zM480 256c0-63.53-32.06-121.94-85.77-156.24-11.19-7.14-26.03-3.82-33.12 7.46s-3.78 26.21 7.41 33.36C408.27 165.97 432 209.11 432 256s-23.73 90.03-63.48 115.42c-11.19 7.14-14.5 22.07-7.41 33.36 6.51 10.36 21.12 15.14 33.12 7.46C447.94 377.94 480 319.54 480 256zm-141.77-76.87c-11.58-6.33-26.19-2.16-32.61 9.45-6.39 11.61-2.16 26.2 9.45 32.61C327.98 228.28 336 241.63 336 256c0 14.38-8.02 27.72-20.92 34.81-11.61 6.41-15.84 21-9.45 32.61 6.43 11.66 21.05 15.8 32.61 9.45 28.23-15.55 45.77-45 45.77-76.88s-17.54-61.32-45.78-76.86z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View file

@ -1 +1,784 @@
Web Remote
<!DOCTYPE html>
<html lang="en">
<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, user-scalable=no, viewport-fit=cover">
<title>Web Remote</title>
<link rel="stylesheet" href="style.css?v=2">
<script src="vue.js"></script>
<script src="sortable.min.js"></script>
<script src="vuedraggable.umd.min.js"></script>
<link rel="manifest" href="./manifest.json?v=2">
</head>
<body oncontextmenu="return false;">
<div id="app" :style="{'--artwork': getAlbumArtUrl()}">
<!-- App view when connected -->
<template v-if="connectedState == 1">
<!-- Streamer Overlay -->
<template></template>
<!-- Mini Player -->
<template v-if="screen == 'miniplayer'">
<div class="miniplayer-main">
<div class="media-artwork--miniplayer" :class="artworkPlaying()"
:style="{'--artwork': getAlbumArtUrl()}">
</div>
<div class="miniplayer-draggable">
</div>
<div class="miniplayer-controls">
<button class="md-btn playback-button--small repeat" @click="repeat()"
v-if="player.currentMediaItem.repeatMode == 0"></button>
<button class="md-btn playback-button--small repeat active" @click="repeat()"
v-else-if="player.currentMediaItem.repeatMode == 2"></button>
<button class="md-btn playback-button--small repeat repeatOne" @click="repeat()"
v-else-if="player.currentMediaItem.repeatMode == 1"></button>
<button class="md-btn playback-button previous" @click="previous()"></button>
<button class="md-btn playback-button pause" @click="pause()"
v-if="player.currentMediaItem.status"></button>
<button class="md-btn playback-button play" @click="play()" v-else></button>
<button class="md-btn playback-button next" @click="next()"></button>
<button class="md-btn playback-button--small shuffle" @click="shuffle()"
v-if="player.currentMediaItem.shuffleMode == 0"></button>
<button class="md-btn playback-button--small shuffle active" @click="shuffle()"
v-else></button>
</div>
</div>
</template>
<!-- Player -->
<transition name="wpfade">
<div class="md-container md-container_panel player-panel" v-if="screen == 'player'">
<div class="player_top">
<div class="md-body player-artwork-container">
<div class="media-artwork" :class="artworkPlaying()" :style="{'--artwork': getAlbumArtUrl()}">
</div>
</div>
</div>
<div class="player_bottom" v-if="player.lowerPanelState == 'lyrics'">
<div class="md-header" style="width:100%;">
<div class="list-entry" v-if="false">
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image" :style="{'--artwork': getAlbumArtUrl()}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ player.currentMediaItem.name }}
</div>
<div class="list-entry-artist">
{{ player.currentMediaItem.artistName }}
</div>
</div>
</div>
</div>
</div>
<div class="md-body lyric-body" style="width:100%;">
<template v-if="player.lyrics">
<template v-for="lyric in player.lyrics" v-if="lyric.line != 'lrcInstrumental'">
<h3 class="lyric-line" @click="seekTo(lyric.startTime, false)"
:class="getLyricClass(lyric.startTime, lyric.endTime)">
{{ lyric.line }}
</h3>
</template>
<template v-else>
<h3 class="lyric-line" @click="seekTo(lyric.startTime, false)"
:class="getLyricClass(lyric.startTime, lyric.endTime)">
<div class="lyricWaiting">
<div></div>
<div></div>
<div></div>
</div>
</h3>
</template>
</template>
<template v-else>
No Lyrics Available
</template>
</div>
<div class="md-footer">
<button class="md-btn playback-button--small lyrics active" @click="player.lowerPanelState = 'controls'"></button>
</div>
</div>
<div class="player_bottom" v-if="player.lowerPanelState == 'controls'">
<div class="md-footer">
<div class="row player-track-info">
<div class="col nopadding text-overflow-elipsis">
<div class="player-song-title text-overflow-elipsis">
{{ player.currentMediaItem.name }}
</div>
<div class="player-song-artist text-overflow-elipsis" @click="searchArtist()">
{{ player.currentMediaItem.artistName }}
</div>
</div>
<div class="col-auto nopadding player-more-container" v-if="false" style="">
<button @click="player.songActions = true;" class="player-more-button">...</button>
</div>
</div>
</div>
<div class="md-footer">
<input type="range" min="0" :value="player.currentMediaItem.durationInMillis - player.currentMediaItem.remainingTime" :max="player.currentMediaItem.durationInMillis" class="web-slider playback-slider" @input="seekTo($event.target.value)">
<div class="row nopadding player-duration-container" style="width: 90%;margin: 0 auto;">
<div class="col nopadding player-duration-time" style="text-align:left">
{{ parseTime(player.currentMediaItem.durationInMillis - player.currentMediaItem.remainingTime) }}
</div>
<div class="col nopadding player-duration-time" style="text-align:right">
-{{ parseTime(player.currentMediaItem.remainingTime) }}
</div>
</div>
</div>
<div class="md-footer playback-buttons">
<button class="md-btn playback-button--small repeat" @click="repeat()" v-if="player.currentMediaItem.repeatMode == 0"></button>
<button class="md-btn playback-button--small repeat active" @click="repeat()" v-else-if="player.currentMediaItem.repeatMode == 2"></button>
<button class="md-btn playback-button--small repeat repeatOne" @click="repeat()" v-else-if="player.currentMediaItem.repeatMode == 1"></button>
<button class="md-btn playback-button previous" @click="previous()"></button>
<button class="md-btn playback-button pause" @click="pause()" v-if="player.currentMediaItem.status"></button>
<button class="md-btn playback-button play" @click="play()" v-else></button>
<button class="md-btn playback-button next" @click="next()"></button>
<button class="md-btn playback-button--small shuffle" @click="shuffle()" v-if="player.currentMediaItem.shuffleMode == 0"></button>
<button class="md-btn playback-button--small shuffle active" @click="shuffle()" v-else></button>
</div>
<div class="md-footer">
<div class="row volume-slider-container">
<div class="col-auto">
<div class="player-volume-glyph decrease"></div>
</div>
<div class="col">
<input type="range" class="web-slider volume-slider" max="1" min="0" step="0.01" @input="setVolume($event.target.value)" :value="player.currentMediaItem.volume">
</div>
<div class="col-auto">
<div class="player-volume-glyph increase"></div>
</div>
</div>
</div>
<div class="md-footer">
<button class="md-btn playback-button--small lyrics" v-if="checkOrientation() == 'portrait'" @click="showLyrics()"></button>
<button class="md-btn playback-button--small lyrics" v-if="checkOrientation() == 'landscape'" @click="showLyricsInline()"></button>
<button class="md-btn playback-button--small queue" @click="showQueue()"></button>
<button class="md-btn playback-button--small search" @click="showSearch()"></button>
</div>
</div>
</div>
</transition>
<!-- Search -->
<transition name="wpfade">
<div class="md-container md-container_panel search-panel" v-if="screen == 'search'">
<div class="search-header">
<div class="md-header">
<div class="row">
<div class="col-auto">
<button class="back-button" @click="screen = 'player'"></button>
</div>
<div class="col" style="display: flex;align-items: center;">
<div class="col">
<input type="text" placeholder="Artists, Songs, Lyrics, and More" spellcheck="false" v-model="search.query" @change="searchQuery()" v-on:keyup.enter="searchQuery()" class="search-input">
</div>
</div>
</div>
</div>
<div class="md-header search-type-container">
<button class="search-type-button" @click="search.searchType = 'applemusic';searchQuery()" :class="searchTypeClass('applemusic')" style="width:100%;">Apple Music
</button>
<button class="search-type-button" @click="search.searchType = 'library';searchQuery()" :class="searchTypeClass('library')" style="width:100%;">Library
</button>
</div>
<div class="md-header search-tab-container" v-if="search.state == 2">
<button class="search-tab" @click="search.tab = 'all'" :class="searchTabClass('all')">All
Results
</button>
<button class="search-tab" @click="search.tab = 'songs'" :class="searchTabClass('songs')">Songs
</button>
<button class="search-tab" @click="search.tab = 'albums'" :class="searchTabClass('albums')">Albums
</button>
<button class="search-tab" @click="search.tab = 'artists'" :class="searchTabClass('artists')">Artists
</button>
</div>
</div>
<div class="search-body-container">
<transition name="wpfade">
<div class="md-body search-body" v-if="search.state == 0">
<div style="font-size: 17px;display:flex;flex-direction: column;justify-content: center;align-items: center;">
<img src="./assets/search.svg" style="width: 40px;margin: 32px;opacity: 0.85"> Search by song, album, artist, or lyrics.
</div>
</div>
</transition>
<transition name="wpfade">
<div class="md-body search-body" v-if="search.state == 1">
<!-- loading state -->
</div>
</transition>
<transition name="wpfade">
<div class="md-body search-body" ref="searchBody" @scroll="searchScroll" style="overflow-y:auto;" v-if="search.state == 2">
<template v-if="canShowSearchTab('songs')">
<div class="list-entry-header">Songs</div>
<div class="list-entry" v-for="song in search.results.songs"
@click="trackSelect(song)">
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image" v-if="song.artwork"
:style="{'--artwork': getAlbumArtUrlList(song.artwork.url)}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ song.name }}
</div>
<div class="list-entry-artist">
{{ song.artistName }}
<span class="lossless-badge" v-if="song.audioTraits.includes('lossless')">Lossless</span>
</div>
</div>
</div>
</div>
<div class="list-entry" v-for="song in search.results['library-songs']"
@click="trackSelect(song)">
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image" v-if="song.artwork"
:style="{'--artwork': getAlbumArtUrlList(song.artwork.url)}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ song.name }}
</div>
<div class="list-entry-artist">
{{ song.artistName }}
</div>
</div>
</div>
</div>
</template>
<template v-if="canShowSearchTab('albums')">
<div class="list-entry-header">Albums</div>
<div class="list-entry" v-for="album in search.results.albums"
@click="showAlbum(album.id)">
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image" v-if="album.artwork"
:style="{'--artwork': getAlbumArtUrlList(album.artwork.url)}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ album.name }}
</div>
<div class="list-entry-artist">
{{ album.artistName }}
<span class="lossless-badge" v-if="album.audioTraits.includes('lossless')">Lossless</span>
</div>
</div>
</div>
</div>
<div class="list-entry" v-for="album in search.results['library-albums']">
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image" v-if="album.artwork"
:style="{'--artwork': getAlbumArtUrlList(album.artwork.url)}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ album.name }}
</div>
<div class="list-entry-artist">
{{ album.artistName }}
</div>
</div>
</div>
</div>
</template>
<template v-if="canShowSearchTab('artists')">
<div class="list-entry-header">Artists</div>
<div class="list-entry"
@click="showArtist(artist.id)"
v-for="artist in search.results.artists"
>
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image artist" v-if="artist.artwork"
:style="{'--artwork': getAlbumArtUrlList(artist.artwork.url)}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ artist.name }}
</div>
<div class="list-entry-artist">
</div>
</div>
</div>
</div>
<div class="list-entry" v-for="artist in search.results['library-artists']">
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image artist" v-if="artist.artwork"
:style="{'--artwork': getAlbumArtUrlList(artist.artwork.url)}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ artist.name }}
</div>
<div class="list-entry-artist">
</div>
</div>
</div>
</div>
</template>
</div>
</transition>
</div>
<footer-player></footer-player>
</div>
</transition>
<!-- Track Select Actions -->
<transition name="wpfade">
<div class="md-container md-container_panel context-menu" style="overflow-y:auto;" v-if="search.trackSelect">
<div class="md-body context-menu-body">
<button class="context-menu-item context-menu-item--left" @click="playMediaItemById(search.selected.id);clearSelectedTrack()">
<div class="row">
<div class="col-auto flex-center" v-if="search.selected.artwork"
style="display:flex;align-items: center;">
<div class="list-entry-image"
:style="{'--artwork': getAlbumArtUrlList(search.selected.artwork.url)}">
</div>
</div>
<div class="col flex-center" style="display:flex;align-items: center;">
<div style="width:100%;font-size: 18px;">
{{ search.selected.name }}
</div>
<div style="width:100%;font-size: 16px;">
{{ search.selected.artistName }}
</div>
<div style="width:100%;font-size: 14px;">
{{ parseTime(search.selected.durationInMillis) }}
</div>
</div>
</div>
</button>
</div>
<div class="md-body context-menu-body" style="height: auto;">
<button class="context-menu-item context-menu-item--left" @click="playMediaItemById(search.selected.id);clearSelectedTrack()">
<div class="row">
<div class="col">
Play
</div>
<div class="col-auto">
▶️
</div>
</div>
</button>
<button class="context-menu-item context-menu-item--left" @click="playNext('song', search.selected.id);clearSelectedTrack()">
<div class="row">
<div class="col">
Play Next
</div>
<div class="col-auto">
⏭️
</div>
</div>
</button>
<button class="context-menu-item context-menu-item--left" @click="playLater('song', search.selected.id);clearSelectedTrack()">
<div class="row">
<div class="col">
Play Later
</div>
<div class="col-auto">
</div>
</div>
</button>
<button class="context-menu-item context-menu-item--left" v-if="false">
<div class="row">
<div class="col">
Open in {{ musicAppVariant() }}
</div>
<div class="col-auto">
🎵
</div>
</div>
</button>
</div>
<div class="md-footer">
<button class="context-menu-item" @click="clearSelectedTrack()">Cancel</button>
</div>
</div>
</transition>
<!-- Song Actions -->
<transition name="wpfade">
<div class="md-container md-container_panel context-menu" v-if="player.songActions">
<div class="md-header">
</div>
<div class="md-body context-menu-body">
<button class="context-menu-item context-menu-item--left" v-if="false">
<div class="row">
<div class="col">
Add To Library
</div>
<div class="col-auto">
</div>
</div>
</button>
<button class="context-menu-item context-menu-item--left" v-if="false">
<div class="row">
<div class="col">
Love
</div>
<div class="col-auto">
❤️
</div>
</div>
</button>
<button class="context-menu-item context-menu-item--left">
<div class="row">
<div class="col">
Share
</div>
<div class="col-auto">
🌐
</div>
</div>
</button>
<button class="context-menu-item context-menu-item--left">
<div class="row">
<div class="col">
Open in {{ musicAppVariant() }}
</div>
<div class="col-auto">
🎵
</div>
</div>
</button>
</div>
<div class="md-footer">
<button class="context-menu-item" @click="player.songActions = false">Cancel</button>
</div>
</div>
</transition>
<!-- Artist Page -->
<transition name="wpfade">
<div class="md-container md-container_panel" v-if="screen == 'artist-page'" v-if="artistPage.data['name']">
<div class="md-header">
<div class="row">
<div class="col-auto">
<button class="back-button" @click="showSearch(true)"></button>
</div>
<div class="col flex-center">
{{ artistPage.data["name"] }}
</div>
</div>
</div>
<div class="album-body-container" :style="getMediaPalette(artistPage.data)">
<div class="artist-header" v-if="artistPage.data['artwork']" :style="getMediaPalette(artistPage.data)">
<div class="artist-header-portrait" :style="{'--artwork': getAlbumArtUrlList(artistPage.data['artwork']['url'], 600)}"></div>
<h2>{{ artistPage.data["name"] }}</h2>
</div>
<div class="md-body artist-body">
<h2>Songs</h2>
<div class="song-scroller-horizontal">
<button v-for="song in artistPage.data['songs']" class="song-placeholder" @click="trackSelect(song)">
{{ song.name }}
</button>
</div>
<h2>Albums</h2>
<div class="mediaitem-scroller-horizontal">
<button v-for="album in artistPage.data['albums']" class="album-placeholder" @click="showAlbum(album.id)">
{{ album.name }}
</button>
</div>
<h2>Playlists</h2>
<div class="mediaitem-scroller-horizontal">
<button v-for="playlist in artistPage.data['playlists']" class="album-placeholder">
{{ playlist.name }}
</button>
</div>
</div>
</div>
<footer-player></footer-player>
</div>
</transition>
<!-- Queue -->
<transition name="wpfade">
<div class="md-container md-container_panel" v-if="screen == 'queue'">
<div class="md-header">
<div class="list-entry" @click="screen = 'player'">
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image" :style="{'--artwork': getAlbumArtUrl()}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ player.currentMediaItem.name }}
</div>
<div class="list-entry-artist">
{{ player.currentMediaItem.artistName }}
</div>
</div>
</div>
</div>
</div>
<div class="md-header" style="text-align: right;padding: 5px 16px;">
<button class="md-btn playback-button--small autoplay" v-if="!player.currentMediaItem.autoplayEnabled" @click="setAutoplay(true)"></button>
<button class="md-btn playback-button--small autoplay activeColor" v-else @click="setAutoplay(false)"></button>
</div>
<div class="md-body queue-body" v-if="!player.queue['_queueItems']">
Empty
</div>
<div class="md-body queue-body" style="overflow-y:auto;" id="list-queue" v-else>
<draggable v-model="queue.temp" handle=".handle" filter=".passed" @change="queueMove">
<template v-for="(song, position) in queue.temp" v-if="position > player.queue['_position']">
<div class="list-entry" :class="getQueuePositionClass(position)">
<div class="row" style="width:100%;">
<div class="col-auto">
<div class="handle">
</div>
</div>
<div class="col-auto flex-center">
<div class="list-entry-image" v-if="song.item.attributes.artwork"
:style="{'--artwork': getAlbumArtUrlList(song.item.attributes.artwork.url)}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ song.item.attributes.name }}
</div>
<div class="list-entry-artist">
{{ song.item.attributes.artistName }}
</div>
</div>
<div class="col-auto flex-center" style="text-align:right;">
<div v-if="position == player.queue['_position']">▶️</div>
</div>
</div>
</div>
</template>
</draggable>
</div>
<div class="md-footer">
<button class="md-btn playback-button--small lyrics" v-if="checkOrientation() == 'portrait'" @click="showLyrics()"></button>
<button class="md-btn playback-button--small lyrics" v-if="checkOrientation() == 'landscape'" @click="screen = 'player';showLyricsInline()"></button>
<button class="md-btn playback-button--small queue active" @click="screen = 'player'"></button>
<button class="md-btn playback-button--small search" @click="showSearch()"></button>
</div>
</div>
</transition>
<!-- Lyrics -->
<transition name="wpfade">
<div class="md-container md-container_panel" v-if="screen == 'lyrics'">
<div class="md-header">
<div class="list-entry" @click="screen = 'player'">
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image" :style="{'--artwork': getAlbumArtUrl()}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ player.currentMediaItem.name }}
</div>
<div class="list-entry-artist">
{{ player.currentMediaItem.artistName }}
</div>
</div>
</div>
</div>
</div>
<div class="md-body lyric-body">
<template v-if="player.lyrics">
<template v-for="lyric in player.lyrics" v-if="lyric.line != 'lrcInstrumental'">
<h3 class="lyric-line" @click="seekTo(lyric.startTime, false)"
:class="getLyricClass(lyric.startTime, lyric.endTime)">
{{ lyric.line }}
</h3>
</template>
<template v-else>
<h3 class="lyric-line" @click="seekTo(lyric.startTime, false)"
:class="getLyricClass(lyric.startTime, lyric.endTime)">
<div class="lyricWaiting">
<div></div>
<div></div>
<div></div>
</div>
</h3>
</template>
</template>
<template v-else>
No Lyrics Available
</template>
</div>
<div class="md-footer">
<button class="md-btn playback-button--small lyrics active" @click="screen = 'player'"></button>
<button class="md-btn playback-button--small queue" @click="showQueue()"></button>
<button class="md-btn playback-button--small search" @click="showSearch()"></button>
</div>
</div>
</transition>
<!-- Album Page -->
<transition name="wpfade">
<div class="md-container md-container_panel md-container_album" v-if="screen == 'album-page' && albumPage.data['name']">
<div class="md-header">
<div class="row">
<div class="col-auto">
<button class="back-button" @click="showSearch(true)"></button>
</div>
</div>
</div>
<div class="album-body-container">
<div class="md-header">
<div class="albumpage-artwork" :style="{'--artwork': getAlbumArtUrlList(albumPage.data['artwork']['url'], 300)}">
</div>
<div class="albumpage-album-name">
{{ albumPage.data["name"] }}
</div>
<div class="albumpage-artist-name" @click="showArtist(albumPage.data['artists'][0]['id'])">
{{ albumPage.data["artistName"] }}
</div>
<div class="albumpage-misc-info">
{{ albumPage.data.genreNames[0] }} ∙ {{ new Date(albumPage.data.releaseDate).getFullYear() }}
</div>
<div class="row" style="margin-top: 20px;">
<div class="col">
<button class="wr-btn" @click="playAlbum(albumPage.data.id, false)" style="width:100%;">Play
</button>
</div>
<div class="col">
<button class="wr-btn" style="width:100%;" @click="playAlbum(albumPage.data.id, true)">Shuffle
</button>
</div>
</div>
<div class="albumpage-album-notes" v-if="albumPage.data['editorialNotes']">
<div class="notes-preview" v-html="albumPage.data['editorialNotes']['standard']">
</div>
<button @click="albumPage.editorsNotes = true" class="notes-more">More</button>
</div>
</div>
<div class="md-body artist-body">
<div class="list-entry-header">Tracks</div>
<div class="list-entry" v-for="song in albumPage.data['tracks']" @click="trackSelect(song)">
<div class="row">
<div class="col-auto flex-center">
<div class="list-entry-image" v-if="song.artwork" :style="{'--artwork': getAlbumArtUrlList(song.artwork.url)}">
</div>
</div>
<div class="col flex-center">
<div class="list-entry-name">
{{ song.name }}
</div>
<div class="list-entry-artist">
{{ song.artistName }}
</div>
</div>
</div>
</div>
<div class="md-footer">
<div>{{ albumPage.data['tracks'].length }} Tracks</div>
<div>
{{ albumPage.data['copyright'] }}
</div>
</div>
</div>
</div>
<footer-player></footer-player>
</div>
</transition>
<!-- Album Page - Editorial Notes -->
<transition name="wpfade">
<div class="md-container md-container_panel context-menu" v-if="albumPage.editorsNotes" style="padding-top: 42px;">
<div class="md-header" :style="getMediaPalette(albumPage.data)" style="font-size: 18px;background:var(--bgColor);color:var(--textColor1);text-align: center;border-radius: 10px 10px 0 0;border-top: 1px solid #ffffff1f;">
{{ albumPage.data["name"] }}
</div>
<div class="md-body album-page-fullnotes-body" :style="getMediaPalette(albumPage.data)" style="background:var(--bgColor);color:var(--textColor1);" v-html="albumPage.data['editorialNotes']['standard']">
</div>
<div class="md-footer" :style="getMediaPalette(albumPage.data)" style="background:var(--bgColor);color:var(--textColor1);">
<button class="context-menu-item" @click="albumPage.editorsNotes = false">Close</button>
</div>
</div>
</transition>
</template>
<!-- Loading -->
<transition name="wpfade">
<div class="md-container md-container_panel connection-error-panel" v-if="connectedState != 1">
<div class="md-header">
</div>
<div class="md-body" style="display:flex;justify-content: center;align-items: center;">
<div v-if="connectedState == 0">
Loading...
</div>
<div v-else>
<h3 style="text-align:center;">Connection Interrupted</h3>
<!--<button class="md-btn md-btn-primary" style="font-weight:500;width: 120px;border-radius: 50px;display:block;margin: 0 auto;" @click="connect()">Retry-->
<button class="md-btn md-btn-primary" style="font-weight:500;width: 120px;border-radius: 50px;display:block;margin: 0 auto;" onclick="document.location = document.location">Retry
</button>
</div>
</div>
<div class="md-footer">
</div>
</div>
</transition>
<!-- Template -->
<transition name="wpfade">
<div class="md-container md-container_panel" v-if="false">
<div class="md-header">
</div>
<div class="md-body">
</div>
<div class="md-footer">
</div>
</div>
</transition>
</div>
<script type="text/x-template" id="footer-player">
<div class="footer-player" v-show="$parent.player.currentMediaItem['name']">
<div class="row" style="width:100%;margin:0px;">
<div class="col-auto flex-center" style="padding:0 6px;" @click="$parent.screen = 'player'">
<div class="list-entry-image" :style="{'--artwork': $parent.getAlbumArtUrl()}">
</div>
</div>
<div class="col flex-center text-overflow-elipsis" @click="$parent.screen = 'player'">
<div class="list-entry-name text-overflow-elipsis">
{{ $parent.player.currentMediaItem.name }}
</div>
<div class="list-entry-artist text-overflow-elipsis">
{{ $parent.player.currentMediaItem.artistName }}
</div>
</div>
<div class="col-auto">
<button class="md-btn playback-button pause" @click="$parent.pause()" v-if="$parent.player.currentMediaItem.status"></button>
<button class="md-btn playback-button play" @click="$parent.play()" v-else></button>
</div>
</div>
</div>
</script>
<script src="./index.js?v=1"></script>
</body>
</html>

618
src/web-remote/index.js Normal file
View file

@ -0,0 +1,618 @@
var socket;
Vue.component('footer-player', {
template: '#footer-player'
});
// vue instance
var app = new Vue({
el: '#app',
data: {
screen: "player",
player: {
currentMediaItem: {},
songActions: false,
lyrics: {},
lyricsMediaItem: {},
lyricsDebug: {
current: 0,
start: 0,
end: 0
},
queue: {},
lowerPanelState: "controls",
userInteraction: false
},
queue: {
temp: []
},
artistPage: {
data: {},
editorsNotes: false
},
albumPage: {
data: {},
editorsNotes: false
},
search: {
query: "",
results: [],
state: 0,
tab: "all",
searchType: "applemusic",
trackSelect: false,
selected: {},
queue: {},
lastPage: "search",
lastY: 0
},
lastPage: "player",
connectedState: 0,
url: window.location.hostname,
mode: "default",
// url: "localhost",
},
methods: {
searchScroll(e) {
this.search.lastY = e.target.scrollTop;
},
musicKitAPI(method, id, params) {
socket.send(
JSON.stringify({
action: "musickit-api",
method: method,
id: id,
params: params
})
)
},
resetPlayerUI() {
this.player.lowerPanelState = "controls";
},
musicAppVariant() {
if (navigator.userAgent.match(/Android/i)) {
return "Cider";
} else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) {
return "Cider";
} else {
if (navigator.userAgent.indexOf('Mac') > 0) {
return 'Music';
} else if (navigator.userAgent.indexOf('Win') > 0) {
return 'Cider';
} else {
return 'Cider';
}
}
},
checkOrientation() {
// check orientation of device
if (window.innerHeight > window.innerWidth) {
return 'portrait'
} else {
return 'landscape';
}
},
checkPlatformMD() {
// check if platfom is desktop or mobile
if (navigator.userAgent.match(/Android/i)) {
return "mobile";
} else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) {
return "mobile";
} else {
if (navigator.userAgent.indexOf('Mac') > 0) {
return 'desktop';
} else if (navigator.userAgent.indexOf('Win') > 0) {
return 'desktop';
} else {
return 'desktop';
}
}
},
checkPlatform() {
if (navigator.userAgent.match(/Android/i)) {
return "android";
} else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) {
return "ios";
} else {
if (navigator.userAgent.indexOf('Mac') > 0) {
return 'mac';
} else if (navigator.userAgent.indexOf('Win') > 0) {
return 'win';
} else {
return 'linux';
}
}
},
artworkPlaying() {
if (this.player.currentMediaItem.status) {
return
} else {
return ["paused"]
}
},
setAutoplay(value) {
socket.send(JSON.stringify({
"action": "set-autoplay",
"autoplay": value
}));
this.getCurrentMediaItem()
if (value) {
setTimeout(() => {
this.getQueue()
}, 1000)
} else {
this.getQueue()
}
},
seekTo(time, adjust = true) {
if (adjust) {
time = parseInt(time / 1000)
}
socket.send(JSON.stringify({
action: "seek",
time: time
}));
},
setVolume(volume) {
socket.send(JSON.stringify({
action: "volume",
volume: volume
}));
},
getQueue() {
socket.send(JSON.stringify({
action: "get-queue"
}))
},
play() {
socket.send(JSON.stringify({
action: "play"
}))
},
pause() {
socket.send(JSON.stringify({
action: "pause"
}))
},
next() {
socket.send(JSON.stringify({
action: "next"
}))
},
previous() {
socket.send(JSON.stringify({
action: "previous"
}))
},
searchArtist() {
this.search.query = this.player.currentMediaItem.artistName;
this.screen = "search";
this.searchQuery();
},
trackSelect(song) {
this.search.selected = song;
this.search.trackSelect = true
},
clearSelectedTrack() {
this.search.selected = {}
this.search.trackSelect = false
},
getArtworkColor(hex) {
return `#${hex}`
},
playMediaItemById(id, kind = "song") {
socket.send(JSON.stringify({
action: "play-mediaitem",
id: id,
kind: kind
}))
this.screen = "player";
},
playNext(type, id) {
socket.send(JSON.stringify({
action: "play-next",
type: type,
id: id
}))
},
playLater(type, id) {
socket.send(JSON.stringify({
action: "play-later",
type: type,
id: id
}))
},
searchQuery() {
if (this.search.query.length == 0) {
this.search.state = 0;
return;
}
this.search.state = 1;
var actionType = "search"
if (this.search.searchType == "library") {
actionType = "library-search"
}
socket.send(JSON.stringify({
"action": actionType,
"term": this.search.query,
"limit": 20
}))
},
quickSearch() {
var search = prompt("Search for a song", "")
if (search == null || search == "") {
return
}
socket.send(JSON.stringify({
action: "quick-play",
term: search
}))
},
parseTime(value) {
var minutes = Math.floor(value / 60000);
var seconds = ((value % 60000) / 1000).toFixed(0);
return minutes + ":" + (seconds < 10 ? '0' : '') + seconds;
},
parseTimeDecimal(value) {
var minutes = Math.floor(value / 60000);
var seconds = ((value % 60000) / 1000).toFixed(0);
return minutes + "." + (seconds < 10 ? '0' : '') + seconds;
},
hmsToSecondsOnly(str) {
var p = str.split(':'),
s = 0,
m = 1;
while (p.length > 0) {
s += m * parseInt(p.pop(), 10);
m *= 60;
}
return s;
},
getCurrentTime() {
return parseFloat(this.hmsToSecondsOnly(this.parseTime(this.player.currentMediaItem.durationInMillis - this.player.currentMediaItem.remainingTime)));
},
percentage(partial, full) {
return (100 * partial) / full
},
getLyricBGStyle(start, end) {
var currentTime = this.getCurrentTime();
var duration = this.player.currentMediaItem.durationInMillis
var start2 = this.hmsToSecondsOnly(start)
var end2 = this.hmsToSecondsOnly(end)
var currentProgress = ((100 * (currentTime)) / (end2))
// check if currenttime is between start and end
this.player.lyricsDebug.start = start2
this.player.lyricsDebug.end = end2
this.player.lyricsDebug.current = currentTime
if (currentTime >= start2 && currentTime <= end2) {
return {
"--bgSpeed": `${(end2 - start2)}s`
}
} else {
return {}
}
},
getLyricClass(start, end) {
var currentTime = this.getCurrentTime();
// check if currenttime is between start and end
if (currentTime >= start && currentTime <= end) {
setTimeout(() => {
if (document.querySelector(".lyric-line.active")) {
document.querySelector(".lyric-line.active").scrollIntoView({
behavior: "smooth",
block: "center"
})
}
}, 200)
return "active"
} else {
return ""
}
},
getAlbumArtUrl(size = 600) {
if (this.player.currentMediaItem.artwork) {
return `url("${this.player.currentMediaItem.artwork.url.replace('{w}', size).replace('{h}', size)}")`;
} else {
return "";
}
},
getAlbumArtUrlList(url, size = 64) {
return `url("${url.replace('{w}', size).replace('{h}', size)}")`;
},
searchTabClass(tab) {
if (tab == this.search.tab) {
return "active";
}
},
searchTypeClass(type) {
if (type == this.search.searchType) {
return "active";
}
},
getQueuePositionClass(position) {
if (this.player.queue["_position"] == position) {
return ["playing", "passed"]
} else if (this.player.queue["_position"] > position) {
return ["passed"]
}
},
showSearch(reset = false) {
if (reset) {
this.search.lastPage = "search"
}
switch (this.search.lastPage) {
case "search":
this.screen = "search"
break;
case "album":
this.screen = "album-page"
break;
case "artist":
this.screen = "artist-page"
break;
case "playlist":
this.screen = "playlist-page"
break;
}
},
showArtistByName(name) {
this.musicKitAPI("search", name, { types: "artists" })
},
showAlbum(id) {
this.search.lastPage = "album"
this.screen = "album-page"
this.musicKitAPI("album", id, {})
},
showArtist(id) {
this.search.lastPage = "artist"
this.screen = "artist-page"
this.musicKitAPI("artist", id, { include: "songs,playlists,albums" })
},
showQueue() {
this.queue.temp = this.player["queue"]["_queueItems"]
this.screen = "queue"
this.getQueue()
},
queueMove(evt) {
console.log(evt)
console.log(`new: ${evt.moved.newIndex} old: ${evt.moved.oldIndex}`)
this.queue.temp.splice(evt.moved.newIndex, 0, this.queue.temp.splice(evt.moved.oldIndex, 1)[0])
socket.send(JSON.stringify({
action: "queue-move",
from: evt.moved.oldIndex,
to: evt.moved.newIndex
}))
this.getQueue()
return true
},
repeat() {
socket.send(JSON.stringify({
action: "repeat"
}))
this.getCurrentMediaItem()
},
shuffle() {
socket.send(JSON.stringify({
action: "shuffle"
}))
this.getCurrentMediaItem()
},
setShuffle(val) {
socket.send(JSON.stringify({
action: "set-shuffle",
shuffle: val
}))
this.getCurrentMediaItem()
},
getMediaPalette(data) {
var palette = {
'--bgColor': `#${data['artwork']['bgColor']}`,
'--textColor1': `#${data['artwork']['textColor1']}`,
'--textColor2': `#${data['artwork']['textColor2']}`,
'--textColor3': `#${data['artwork']['textColor3']}`,
'--textColor4': `#${data['artwork']['textColor4']}`
}
return palette
},
playAlbum(id, shuffle = false) {
if (shuffle) {
this.setShuffle(true)
} else {
this.setShuffle(false)
}
this.playMediaItemById(id, 'album');
},
getLyrics() {
socket.send(JSON.stringify({
action: "get-lyrics",
}))
},
showLyrics() {
this.getLyrics()
this.screen = "lyrics"
},
showLyricsInline() {
this.getLyrics()
this.player.lowerPanelState = "lyrics"
},
parseLyrics() {
var xml = this.stringToXml(this.player.lyricsMediaItem.ttml)
var json = xmlToJson(xml);
this.player.lyrics = json
},
stringToXml(st) {
// string to xml
var xml = (new DOMParser()).parseFromString(st, "text/xml");
return xml;
},
canShowSearchTab(tab) {
if (tab == this.search.tab || this.search.tab == "all") {
return true;
} else {
return false;
}
},
getCurrentMediaItem() {
socket.send(JSON.stringify({
action: "get-currentmediaitem"
}))
},
setStreamerOverlay() {
document.body.classList.add("streamer-overlay")
},
setMode(mode) {
switch (mode) {
default: this.screen = "player"
break;
case "miniplayer":
this.screen = "miniplayer"
break;
}
},
connect() {
let self = this;
this.connectedState = 0;
if (this.url === "") {
this.url = prompt("Host IP", "localhost")
}
socket = new WebSocket(`ws://${this.url}:26369`);
socket.onopen = (e) => {
console.log(e);
console.log('connected');
app.connectedState = 1;
if (getParameterByName("mode")) {
self.setMode(getParameterByName("mode"))
} else {
self.setMode("default")
}
self.clearSelectedTrack()
}
socket.onclose = (e) => {
console.log(e);
console.log('disconnected');
app.connectedState = 2;
}
socket.onerror = (e) => {
console.log(e);
console.log('error');
app.connectedState = 2;
}
socket.onmessage = (e) => {
console.log(e.data)
const response = JSON.parse(e.data);
switch (response.type) {
default: console.log(response);
break;
case "musickitapi.search":
self.showArtist(response.data["artists"][0]["id"]);
break;
case "musickitapi.album":
if (self.screen == "album-page") {
self.albumPage.data = response.data
}
break;
case "musickitapi.artist":
if (self.screen == "artist-page") {
self.artistPage.data = response.data
}
break;
case "queue":
self.player.queue = response.data;
self.queue.temp = response.data["_queueItems"];
self.$forceUpdate()
break;
case "lyrics":
self.player.lyrics = response.data;
self.$forceUpdate()
break;
case "searchResultsLibrary":
self.search.results = response.data;
self.search.state = 2;
break;
case "searchResults":
self.search.results = response.data;
self.search.state = 2;
break;
case "playbackStateUpdate":
if (!self.player.userInteraction) {
self.updatePlaybackState(response.data)
}
break;
}
// console.log(e.data);
}
},
updatePlaybackState(mediaitem) {
var lyricsDisplayed = this.screen == "lyrics" || this.player.lowerPanelState == "lyrics"
if (this.player.currentMediaItem["isrc"] != mediaitem["isrc"]) {
if (lyricsDisplayed) {
this.getLyrics()
}
if (this.screen == "queue") {
this.getQueue()
}
}
this.player.currentMediaItem = mediaitem
}
},
});
function getParameterByName(name, url) {
if (!url) url = window.location.href;
name = name.replace(/[\[\]]/g, '\\$&');
var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, ' '));
}
function xmlToJson(xml) {
// Create the return object
var obj = {};
if (xml.nodeType == 1) { // element
// do attributes
if (xml.attributes.length > 0) {
obj["@attributes"] = {};
for (var j = 0; j < xml.attributes.length; j++) {
var attribute = xml.attributes.item(j);
obj["@attributes"][attribute.nodeName] = attribute.nodeValue;
}
}
} else if (xml.nodeType == 3) { // text
obj = xml.nodeValue;
}
// do children
if (xml.hasChildNodes()) {
for (var i = 0; i < xml.childNodes.length; i++) {
var item = xml.childNodes.item(i);
var nodeName = item.nodeName;
if (typeof(obj[nodeName]) == "undefined") {
obj[nodeName] = xmlToJson(item);
} else {
if (typeof(obj[nodeName].push) == "undefined") {
var old = obj[nodeName];
obj[nodeName] = [];
obj[nodeName].push(old);
}
obj[nodeName].push(xmlToJson(item));
}
}
}
return obj;
};
window.onresize = function() {
app.resetPlayerUI()
}
app.connect()

View file

@ -0,0 +1,42 @@
{
"theme_color": "#000000",
"background_color": "#000000",
"display": "standalone",
"scope": "/",
"start_url": "/",
"name": "Cider Remote",
"short_name": "Cider Remote",
"description": "Cider Remote",
"developer": {
"name": "Cider Collective",
"url": "https://cider.sh?utm-source=manifest"
},
"homepage_url": "https://cider.sh?utm-source=manifest",
"icons": [{
"src": "/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-256x256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"protocol_handlers": [{
"protocol": "ext+cider",
"name": "Cider",
"uriTemplate": "/?url=%s"
}]
}

2
src/web-remote/sortable.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1023
src/web-remote/style.css Normal file

File diff suppressed because it is too large Load diff

6
src/web-remote/vue.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

18
tsconfig.json Normal file
View file

@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"noImplicitAny": true,
"strict": true,
"sourceMap": true,
"outDir": "./build",
"baseUrl": ".",
"paths": {
"*": ["node_modules/*"]
},
"allowSyntheticDefaultImports": true
},
"include": [
"src/**/*"
]
}