Revamp Music API, add E2E, update Spectrum and User

Music model: new method structure, updated API endpoints (tracks,
releases, lyrics, library).
E2E (wip): new model for encryption key pair management.
Spectrum: updated API to support new
spectrum api.
User: added public key management.
Session, Auth, Radio: minor API enhancements.
This commit is contained in:
SrGooglo 2025-05-10 02:35:38 +00:00
parent 511a81e313
commit e035e6bde4
23 changed files with 453 additions and 533 deletions

View File

@ -42,7 +42,6 @@ export function createClient({
mainOrigin: origin,
baseRequest: null,
ws: null,
rest: null,
version: pkg.version,
addons: new AddonsManager(),
})

View File

@ -35,7 +35,7 @@ export default class AuthModel {
SessionModel.refreshToken = response.data.refreshToken
if (typeof callback === "function") {
await callback()
await callback(response.data)
}
__comty_shared_state.eventBus.emit("auth:login_success")

30
src/models/e2e/index.js Normal file
View File

@ -0,0 +1,30 @@
import SessionModel from "../session"
import request from "../../request"
export default class E2EModel {
static async getKeyPair() {
const response = await request({
method: "GET",
url: "/users/self/keypair",
})
return response.data
}
// WARNING: updating keypair makes all decryption fail
static async updateKeyPair(str, { imSure = false } = {}) {
if (imSure !== true) {
throw new Error(
"Missing confirmation to update the keypair. Use `imSure = true` to proceed.",
)
}
const response = await request({
method: "POST",
url: "/users/self/keypair",
data: { str: str },
})
return response.data
}
}

View File

@ -1,11 +0,0 @@
import request from "../../../request"
export default async () => {
const response = await request({
method: "GET",
url: "/music/playlists/featured",
})
// @ts-ignore
return response.data
}

View File

@ -0,0 +1,25 @@
import request from "../../../request"
export default async (type, item_id) => {
if (!type) {
throw new Error("type is required")
}
if (!item_id) {
throw new Error("item_id is required")
}
type = type.toLowerCase()
const response = await request({
method: "GET",
url: `/music/my/library/favorite`,
params: {
kind: type,
item_id: item_id,
},
})
// @ts-ignore
return response.data
}

View File

@ -1,33 +0,0 @@
import request from "../../../request"
const typeToNamespace = {
track: "tracks",
//playlist: "playlists",
//release: "releases",
}
export default async (type, track_id) => {
if (!type) {
throw new Error("type is required")
}
if (!track_id) {
throw new Error("track_id is required")
}
type = type.toLowerCase()
type = typeToNamespace[type]
if (!type) {
throw new Error(`Unsupported type: ${type}`)
}
const response = await request({
method: "GET",
url: `/music/${type}/${track_id}/is_favourite`,
})
// @ts-ignore
return response.data
}

View File

@ -2,24 +2,24 @@ import request from "../../../request"
import processAddons from "../../../helpers/processWithAddons"
import standartListMerge from "../../../utils/standartListMerge"
export default async ({ limit = 100, offset = 0, order = "desc" }) => {
const addons =
__comty_shared_state.addons.getByOperation("getFavoriteFolder")
export default async ({ limit = 100, offset = 0, order = "desc", kind }) => {
const addons = __comty_shared_state.addons.getByOperation("getMyLibrary")
const dividedLimit = limit / (addons.length + 1)
const { data } = await request({
method: "GET",
url: "/music/my/folder",
url: "/music/my/library",
params: {
limit: dividedLimit,
offset: offset,
order: order,
kind: kind,
},
})
let results = await processAddons({
operation: "getFavoriteFolder",
operation: "getMyLibrary",
initialData: data,
fnArguments: [{ limit: dividedLimit, offset: offset, order: order }],
normalizeAddonResult: ({ currentData, addonResult }) => {
@ -27,7 +27,8 @@ export default async ({ limit = 100, offset = 0, order = "desc" }) => {
},
})
// sort by liked_at
// sort tracks by liked_at
if (results.tracks) {
results.tracks.items.sort((a, b) => {
if (a.liked_at > b.liked_at) {
return -1
@ -37,6 +38,7 @@ export default async ({ limit = 100, offset = 0, order = "desc" }) => {
}
return 0
})
}
return results
}

View File

@ -6,19 +6,15 @@ type Arguments = {
keywords: String
}
export default async ({
limit,
offset,
keywords,
}: Arguments) => {
export default async ({ limit, offset, keywords }: Arguments) => {
const response = await request({
method: "GET",
url: "/music/releases/self",
url: "/music/my/releases",
params: {
limit: limit,
offset: offset,
keywords: keywords,
}
},
})
// @ts-ignore

View File

@ -1,11 +0,0 @@
import request from "../../../request"
export default async (id: String) => {
const response = await request({
method: "GET",
url: `/music/playlists/${id}/data`,
})
// @ts-ignore
return response.data
}

View File

@ -1,11 +0,0 @@
import request from "../../../request"
export default async (id: String) => {
const response = await request({
method: "GET",
url: `/music/playlists/${id}/items`,
})
// @ts-ignore
return response.data
}

View File

@ -1,29 +0,0 @@
import request from "../../../request"
type Arguments = {
keywords: String
user_id: String
limit: Number
offset: Number
}
export default async ({
keywords,
user_id,
limit,
offset,
}: Arguments) => {
const response = await request({
method: "GET",
url: "/music/playlists",
params: {
keywords: keywords,
user_id: user_id,
limit: limit,
offset: offset,
}
})
// @ts-ignore
return response.data
}

View File

@ -1,27 +1,20 @@
import request from "../../../request"
type Arguments = {
keywords: String
user_id: String
limit: Number
offset: Number
page: Number
}
export default async ({
keywords,
user_id,
limit,
offset,
}: Arguments) => {
export default async ({ user_id, limit, page }: Arguments) => {
const response = await request({
method: "GET",
url: "/music/releases",
params: {
keywords: keywords,
user_id: user_id,
limit: limit,
offset: offset,
}
page: page,
},
})
// @ts-ignore

View File

@ -12,7 +12,7 @@ export default async (
id: String,
options: RequestOptions = {
preferTranslation: false,
}
},
) => {
const requestParams: RequestParams = Object()
@ -22,8 +22,8 @@ export default async (
const response = await request({
method: "GET",
url: `/music/lyrics/${id}`,
params: requestParams
url: `/music/tracks/${id}/lyrics`,
params: requestParams,
})
// @ts-ignore

View File

@ -1,27 +1,20 @@
import request from "../../../request"
type Arguments = {
keywords: String
user_id: String
limit: Number
offset: Number
page: Number
}
export default async ({
keywords,
user_id,
limit,
offset,
}: Arguments) => {
export default async ({ user_id, limit, page }: Arguments) => {
const response = await request({
method: "GET",
url: "/music/tracks",
params: {
keywords: keywords,
user_id: user_id,
limit: limit,
offset: offset,
}
page: page,
},
})
// @ts-ignore

View File

@ -5,147 +5,34 @@ export default class MusicModel {
static Getters = Getters
static Setters = Setters
/**
* Performs a search based on the provided keywords, with optional parameters for limiting the number of results and pagination.
*
* @param {string} keywords - The keywords to search for.
* @param {object} options - An optional object containing additional parameters.
* @param {number} options.limit - The maximum number of results to return. Defaults to 5.
* @param {number} options.offset - The offset to start returning results from. Defaults to 0.
* @param {boolean} options.useTidal - Whether to use Tidal for the search. Defaults to false.
* @return {Promise<Object>} The search results.
*/
static search = Getters.search
/**
* Retrieves playlist items based on the provided parameters.
*
* @param {Object} options - The options object.
* @param {string} options.playlist_id - The ID of the playlist.
* @param {string} options.service - The service from which to retrieve the playlist items.
* @param {number} options.limit - The maximum number of items to retrieve.
* @param {number} options.offset - The number of items to skip before retrieving.
* @return {Promise<Object>} Playlist items data.
*/
static getPlaylistItems = Getters.PlaylistItems
/**
* Retrieves playlist data based on the provided parameters.
*
* @param {Object} options - The options object.
* @param {string} options.playlist_id - The ID of the playlist.
* @param {string} options.service - The service to use.
* @param {number} options.limit - The maximum number of items to retrieve.
* @param {number} options.offset - The offset for pagination.
* @return {Promise<Object>} Playlist data.
*/
static getPlaylistData = Getters.PlaylistData
/**
* Retrieves releases based on the provided parameters.
* If user_id is not provided, it will retrieve self authenticated user releases.
*
* @param {object} options - The options for retrieving releases.
* @param {string} options.user_id - The ID of the user.
* @param {string[]} options.keywords - The keywords to filter releases by.
* @param {number} options.limit - The maximum number of releases to retrieve.
* @param {number} options.offset - The offset for paginated results.
* @return {Promise<Object>} - A promise that resolves to the retrieved releases.
*/
static getReleases = Getters.releases
/**
* Retrieves self releases.
*
* @param {object} options - The options for retrieving my releases.
* @param {number} options.limit - The maximum number of releases to retrieve.
* @param {number} options.offset - The offset for paginated results.
* @return {Promise<Object>} - A promise that resolves to the retrieved releases.
*/
static getMyReleases = Getters.myReleases
/**
* Retrieves release data by ID.
*
* @param {number} id - The ID of the release.
* @return {Promise<Object>} The release data.
*/
static getReleaseData = Getters.releaseData
/**
* Retrieves track data for a given ID.
*
* @param {string} id - The ID of the track or multiple IDs separated by commas.
* @return {Promise<Object>} The track data.
*/
// track related methods
static getMyTracks = null
static getAllTracks = Getters.tracks
static getTrackData = Getters.trackData
static putTrack = Setters.putTrack
static deleteTrack = null
/**
* Retrieves the official featured playlists.
*
* @return {Promise<Object>} The data containing the featured playlists.
*/
static getFeaturedPlaylists = Getters.featuredPlaylists
/**
* Retrieves track lyrics for a given ID.
*
* @param {string} id - The ID of the track.
* @return {Promise<Object>} The track lyrics.
*/
// lyrics related methods
static getTrackLyrics = Getters.trackLyrics
static putTrackLyrics = Setters.putTrackLyrics
/**
* Create or modify a track.
*
* @param {object} TrackManifest - The track manifest.
* @return {Promise<Object>} The result track data.
*/
static putTrack = Setters.putTrack
/**
* Create or modify a release.
*
* @param {object} ReleaseManifest - The release manifest.
* @return {Promise<Object>} The result release data.
*/
// release related methods
static getMyReleases = Getters.myReleases
static getAllReleases = Getters.releases
static getReleaseData = Getters.releaseData
static putRelease = Setters.putRelease
/**
* Deletes a release by its ID.
*
* @param {string} id - The ID of the release to delete.
* @return {Promise<Object>} - A Promise that resolves to the data returned by the API.
*/
static deleteRelease = Setters.deleteRelease
/**
* Retrieves the favourite tracks of the current user.
*
* @return {Promise<Object>} The favorite tracks data.
*/
static getFavouriteTracks = null
/**
* Retrieves the favourite tracks/playlists/releases of the current user.
*
* @return {Promise<Object>} The favorite playlists data.
*/
static getFavouriteFolder = Getters.favouriteFolder
/**
* Toggles the favourite status of a track, playlist or folder.
*
* @param {string} track_id - The ID of the track to toggle the favorite status.
* @throws {Error} If the track_id is not provided.
* @return {Promise<Object>} The response data after toggling the favorite status.
*/
static toggleItemFavourite = Setters.toggleItemFavourite
static isItemFavourited = Getters.isItemFavourited
// library related methods
static getMyLibrary = Getters.library
static toggleItemFavorite = Setters.toggleItemFavorite
static isItemFavorited = Getters.isItemFavorited
// other methods
static getRecentyPlayed = Getters.recentlyPlayed
static search = Getters.search
// aliases
static toggleItemFavourite = MusicModel.toggleItemFavorite
static isItemFavourited = MusicModel.isItemFavorited
}

View File

@ -3,7 +3,7 @@ import request from "../../../request"
export default async (track_id, data) => {
const response = await request({
method: "put",
url: `/music/lyrics/${track_id}`,
url: `/music/tracks/${track_id}/lyrics`,
data: data,
})

View File

@ -0,0 +1,26 @@
import request from "../../../request"
export default async (type, item_id, to) => {
if (!type) {
throw new Error("type is required")
}
if (!item_id) {
throw new Error("item_id is required")
}
type = type.toLowerCase()
const response = await request({
method: "PUT",
url: `/music/my/library/favorite`,
data: {
item_id: item_id,
kind: type,
to: to,
},
})
// @ts-ignore
return response.data
}

View File

@ -1,36 +0,0 @@
import request from "../../../request"
const typeToNamespace = {
track: "tracks",
//playlist: "playlists",
//release: "releases",
}
export default async (type, track_id, to) => {
if (!type) {
throw new Error("type is required")
}
if (!track_id) {
throw new Error("track_id is required")
}
type = type.toLowerCase()
type = typeToNamespace[type]
if (!type) {
throw new Error(`Unsupported type: ${type}`)
}
const response = await request({
method: "post",
url: `/music/${type}/${track_id}/favourite`,
data: {
to: to,
}
})
// @ts-ignore
return response.data
}

View File

@ -0,0 +1,10 @@
import request from "../../../request"
export default async () => {
const { data } = await request({
method: "GET",
url: "/music/radio/trendings",
})
return data
}

View File

@ -1,5 +1,7 @@
import getRadioList from "./getters/list"
import getTrendings from "./getters/trendings"
export default class Radio {
static getRadioList = getRadioList
static getTrendings = getTrendings
}

View File

@ -99,7 +99,7 @@ export default class Session {
static async getAllSessions() {
const response = await request({
method: "get",
url: "/sessions/all"
url: "/sessions/all",
})
return response.data
@ -113,7 +113,7 @@ export default class Session {
static async getCurrentSession() {
const response = await request({
method: "get",
url: "/sessions/current"
url: "/sessions/current",
})
return response.data
@ -134,7 +134,7 @@ export default class Session {
const response = await request({
method: "delete",
url: "/sessions/current"
url: "/sessions/current",
}).catch((error) => {
console.error(error)
@ -148,8 +148,13 @@ export default class Session {
return response.data
}
static async destroyAllSessions() {
throw new Error("Not implemented")
static async destroyAll() {
const response = await request({
method: "delete",
url: "/sessions/all",
})
return response.data
}
/**

View File

@ -1,8 +1,8 @@
import axios from "axios"
import { RTEngineClient } from "linebridge-client/src"
import SessionModel from "../session"
import UserModel from "../user"
import { RTEngineClient } from "linebridge-client/src"
//import { RTEngineClient } from "../../../../linebridge/client/src"
async function injectUserDataOnList(list) {
if (!Array.isArray(list)) {
@ -73,7 +73,7 @@ export default class Streaming {
const { data } = await Streaming.base({
method: "get",
url: `/streaming/${stream_id}`,
url: `/stream/${stream_id}/data`,
})
return data
@ -81,37 +81,51 @@ export default class Streaming {
static async getOwnProfiles() {
const { data } = await Streaming.base({
method: "get",
method: "GET",
url: "/streaming/profiles/self",
})
return data
}
static async getProfile({ profile_id }) {
static async getProfile(profile_id) {
if (!profile_id) {
return null
}
const { data } = await Streaming.base({
method: "get",
method: "GET",
url: `/streaming/profiles/${profile_id}`,
})
return data
}
static async createOrUpdateProfile(update) {
static async createProfile(payload) {
const { data } = await Streaming.base({
method: "put",
url: `/streaming/profiles/self`,
method: "POST",
url: "/streaming/profiles/new",
data: payload,
})
return data
}
static async updateProfile(profile_id, update) {
if (!profile_id) {
return null
}
const { data } = await Streaming.base({
method: "PUT",
url: `/streaming/profiles/${profile_id}`,
data: update,
})
return data
}
static async deleteProfile({ profile_id }) {
static async deleteProfile(profile_id) {
if (!profile_id) {
return null
}
@ -124,6 +138,36 @@ export default class Streaming {
return data
}
static async addRestreamToProfile(profileId, restreamData) {
if (!profileId) {
console.error("profileId is required to add a restream")
return null
}
const { data } = await Streaming.base({
method: "put",
url: `/streaming/profiles/${profileId}/restreams`,
data: restreamData,
})
return data
}
static async deleteRestreamFromProfile(profileId, restreamIndexData) {
if (!profileId) {
console.error("profileId is required to delete a restream")
return null
}
const { data } = await Streaming.base({
method: "delete",
url: `/streaming/profiles/${profileId}/restreams`,
data: restreamIndexData,
})
return data
}
static async list({ limit, offset } = {}) {
let { data } = await Streaming.base({
method: "get",
@ -145,11 +189,7 @@ export default class Streaming {
return null
}
const client = new RTEngineClient({
...params,
url: Streaming.apiHostname,
token: SessionModel.token,
})
const client = Streaming.createWebsocket(params)
client._destroy = client.destroy
@ -171,4 +211,14 @@ export default class Streaming {
return client
}
static createWebsocket(params = {}) {
const client = new RTEngineClient({
...params,
url: Streaming.apiHostname,
token: SessionModel.token,
})
return client
}
}

View File

@ -14,7 +14,15 @@ export default class User {
let { username, user_id, basic = false } = payload
if (!username && !user_id) {
user_id = SessionModel.user_id
const response = await request({
method: "GET",
url: `/users/self`,
params: {
basic,
},
})
return response.data
}
if (username && !user_id) {
@ -132,4 +140,29 @@ export default class User {
return data
}
static async getPublicKey(user_id) {
if (!user_id) {
user_id = SessionModel.user_id
}
const { data } = await request({
method: "GET",
url: `/users/${user_id}/public-key`,
})
return data
}
static async updatePublicKey(public_key) {
const { data } = await request({
method: "PUT",
url: `/users/self/public-key`,
data: {
public_key: public_key,
},
})
return data
}
}