mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 10:34:17 +00:00
Supporting multiplatform track likes
This commit is contained in:
parent
b925915a66
commit
76819e99b2
@ -1,7 +1,6 @@
|
||||
import React from "react"
|
||||
import * as antd from "antd"
|
||||
import classnames from "classnames"
|
||||
import seekToTimeLabel from "utils/seekToTimeLabel"
|
||||
|
||||
import { ImageViewer } from "components"
|
||||
import { Icons } from "components/Icons"
|
||||
@ -13,6 +12,17 @@ import { Context as PlaylistContext } from "contexts/WithPlaylistContext"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
const handlers = {
|
||||
"like": async (ctx, track) => {
|
||||
app.cores.player.toggleCurrentTrackLike(true, track)
|
||||
ctx.closeMenu()
|
||||
},
|
||||
"unlike": async (ctx, track) => {
|
||||
app.cores.player.toggleCurrentTrackLike(false, track)
|
||||
ctx.closeMenu()
|
||||
},
|
||||
}
|
||||
|
||||
const Track = (props) => {
|
||||
const {
|
||||
track_manifest,
|
||||
@ -55,8 +65,19 @@ const Track = (props) => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleMoreMenuItemClick = () => {
|
||||
const handleMoreMenuItemClick = (e) => {
|
||||
const { key } = e
|
||||
|
||||
if (typeof handlers[key] === "function") {
|
||||
return handlers[key](
|
||||
{
|
||||
closeMenu: () => {
|
||||
setMoreMenuOpened(false)
|
||||
}
|
||||
},
|
||||
props.track
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const moreMenuItems = React.useMemo(() => {
|
||||
@ -70,19 +91,30 @@ const Track = (props) => {
|
||||
key: "share",
|
||||
icon: <Icons.MdShare />,
|
||||
label: "Share",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: "add_to_playlist",
|
||||
icon: <Icons.MdPlaylistAdd />,
|
||||
label: "Add to playlist",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: "add_to_queue",
|
||||
icon: <Icons.MdQueueMusic />,
|
||||
label: "Add to queue",
|
||||
disabled: true,
|
||||
}
|
||||
]
|
||||
|
||||
if (props.track.liked) {
|
||||
items[0] = {
|
||||
key: "unlike",
|
||||
icon: <Icons.MdFavorite />,
|
||||
label: "Unlike",
|
||||
}
|
||||
}
|
||||
|
||||
if (playlist_ctx) {
|
||||
if (playlist_ctx.owning_playlist) {
|
||||
items.push({
|
||||
@ -98,7 +130,7 @@ const Track = (props) => {
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
}, [props.track])
|
||||
|
||||
return <div
|
||||
id={props.track._id}
|
||||
|
@ -18,8 +18,8 @@ const EventsHandlers = {
|
||||
"playback": () => {
|
||||
return app.cores.player.playback.toggle()
|
||||
},
|
||||
"like": () => {
|
||||
|
||||
"like": async (ctx) => {
|
||||
await app.cores.player.toggleCurrentTrackLike(!ctx.track_manifest?.liked)
|
||||
},
|
||||
"previous": () => {
|
||||
return app.cores.player.playback.previous()
|
||||
|
37
packages/app/src/components/Player/ExtraActions/index.jsx
Normal file
37
packages/app/src/components/Player/ExtraActions/index.jsx
Normal file
@ -0,0 +1,37 @@
|
||||
import React from "react"
|
||||
import { Button } from "antd"
|
||||
import { Icons } from "components/Icons"
|
||||
|
||||
import LikeButton from "components/LikeButton"
|
||||
import { Context } from "contexts/WithPlayerContext"
|
||||
|
||||
const ExtraActions = (props) => {
|
||||
const ctx = React.useContext(Context)
|
||||
|
||||
const handleClickLike = async () => {
|
||||
await app.cores.player.toggleCurrentTrackLike(!ctx.track_manifest?.liked)
|
||||
}
|
||||
|
||||
return <div className="extra_actions">
|
||||
{
|
||||
app.isMobile && <Button
|
||||
type="ghost"
|
||||
icon={<Icons.MdAbc />}
|
||||
disabled={!ctx.track_manifest.lyricsEnabled}
|
||||
/>
|
||||
}
|
||||
{
|
||||
!app.isMobile && <LikeButton
|
||||
liked={ctx.track_manifest?.liked ?? false}
|
||||
onClick={handleClickLike}
|
||||
/>
|
||||
}
|
||||
|
||||
<Button
|
||||
type="ghost"
|
||||
icon={<Icons.MdQueueMusic />}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default ExtraActions
|
@ -13,7 +13,7 @@ import Controls from "components/Player/Controls"
|
||||
|
||||
import RGBStringToValues from "utils/rgbToValues"
|
||||
|
||||
import LikeButton from "components/LikeButton"
|
||||
import ExtraActions from "../ExtraActions"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
@ -45,17 +45,6 @@ const ServiceIndicator = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const ExtraActions = (props) => {
|
||||
return <div className="extra_actions">
|
||||
<LikeButton />
|
||||
|
||||
<antd.Button
|
||||
type="ghost"
|
||||
icon={<Icons.MdQueueMusic />}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
const Player = (props) => {
|
||||
const ctx = React.useContext(Context)
|
||||
|
||||
|
@ -13,7 +13,7 @@ import AudioPlayerStorage from "./player.storage"
|
||||
import defaultAudioProccessors from "./processors"
|
||||
|
||||
import MediaSession from "./mediaSession"
|
||||
import servicesToManifestResolver from "./servicesToManifestResolver"
|
||||
import ServicesHandlers from "./services"
|
||||
|
||||
export default class Player extends Core {
|
||||
static dependencies = [
|
||||
@ -94,6 +94,7 @@ export default class Player extends Core {
|
||||
seek: this.seek.bind(this),
|
||||
minimize: this.toggleMinimize.bind(this),
|
||||
collapse: this.toggleCollapse.bind(this),
|
||||
toggleCurrentTrackLike: this.toggleCurrentTrackLike.bind(this),
|
||||
state: new Proxy(this.state, {
|
||||
get: (target, prop) => {
|
||||
return target[prop]
|
||||
@ -127,6 +128,10 @@ export default class Player extends Core {
|
||||
},
|
||||
}
|
||||
|
||||
wsEvents = {
|
||||
|
||||
}
|
||||
|
||||
async onInitialize() {
|
||||
this.native_controls.initialize()
|
||||
|
||||
@ -317,11 +322,16 @@ export default class Player extends Core {
|
||||
|
||||
// check if manifest has `manifest` property, if is and not inherit or missing source, resolve
|
||||
if (manifest.service) {
|
||||
if (!ServicesHandlers[manifest.service]) {
|
||||
this.console.error(`Service ${manifest.service} is not supported`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (manifest.service !== "inherit" && !manifest.source) {
|
||||
const resolver = servicesToManifestResolver[manifest.service]
|
||||
const resolver = ServicesHandlers[manifest.service].resolve
|
||||
|
||||
if (!resolver) {
|
||||
this.console.error(`Service ${manifest.service} is not supported`)
|
||||
this.console.error(`Resolving for service [${manifest.service}] is not supported`)
|
||||
return false
|
||||
}
|
||||
|
||||
@ -536,6 +546,8 @@ export default class Player extends Core {
|
||||
// play
|
||||
await this.track_instance.media.play()
|
||||
|
||||
this.console.log(this.track_instance)
|
||||
|
||||
// update manifest
|
||||
this.state.track_manifest = instance.manifest
|
||||
|
||||
@ -960,4 +972,38 @@ export default class Player extends Core {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async toggleCurrentTrackLike(to, manifest) {
|
||||
let isCurrent = !!!manifest
|
||||
|
||||
if (typeof manifest === "undefined") {
|
||||
manifest = this.track_instance.manifest
|
||||
}
|
||||
|
||||
if (!manifest) {
|
||||
this.console.error("Track instance or manifest not found")
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof to !== "boolean") {
|
||||
this.console.warn("Like must be a boolean")
|
||||
return false
|
||||
}
|
||||
|
||||
const service = manifest.service ?? "default"
|
||||
|
||||
if (!ServicesHandlers[service].toggleLike) {
|
||||
this.console.error(`Service [${service}] does not support like actions`)
|
||||
return false
|
||||
}
|
||||
|
||||
const result = await ServicesHandlers[service].toggleLike(manifest, to)
|
||||
|
||||
if (isCurrent) {
|
||||
this.track_instance.manifest.liked = to
|
||||
this.state.track_manifest.liked = to
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
35
packages/app/src/cores/player/services.js
Normal file
35
packages/app/src/cores/player/services.js
Normal file
@ -0,0 +1,35 @@
|
||||
import SyncModel from "comty.js/models/sync"
|
||||
import MusicModel from "comty.js/models/music"
|
||||
|
||||
export default {
|
||||
"default": {
|
||||
resolve: () => { },
|
||||
toggleLike: async (manifest, to) => {
|
||||
return await MusicModel.toggleTrackLike(manifest, to)
|
||||
}
|
||||
},
|
||||
"tidal": {
|
||||
resolve: async (manifest) => {
|
||||
const resolvedManifest = await SyncModel.tidalCore.getTrackManifest(manifest.id)
|
||||
|
||||
manifest.source = resolvedManifest.playback.url
|
||||
|
||||
if (!manifest.metadata) {
|
||||
manifest.metadata = {}
|
||||
}
|
||||
|
||||
manifest.metadata.title = resolvedManifest.metadata.title
|
||||
manifest.metadata.artist = resolvedManifest.metadata.artists.map(artist => artist.name).join(", ")
|
||||
manifest.metadata.album = resolvedManifest.metadata.album.title
|
||||
|
||||
const coverUID = resolvedManifest.metadata.album.cover.replace(/-/g, "/")
|
||||
|
||||
manifest.metadata.cover = `https://resources.tidal.com/images/${coverUID}/1280x1280.jpg`
|
||||
|
||||
return manifest
|
||||
},
|
||||
toggleLike: async (manifest, to) => {
|
||||
return await MusicModel.toggleTrackLike(manifest, to)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
import SyncModel from "comty.js/models/sync"
|
||||
|
||||
export default {
|
||||
"tidal": async (manifest) => {
|
||||
const resolvedManifest = await SyncModel.tidalCore.getTrackManifest(manifest.id)
|
||||
|
||||
manifest.source = resolvedManifest.playback.url
|
||||
|
||||
if (!manifest.metadata) {
|
||||
manifest.metadata = {}
|
||||
}
|
||||
|
||||
manifest.metadata.title = resolvedManifest.metadata.title
|
||||
manifest.metadata.artist = resolvedManifest.metadata.artists.map(artist => artist.name).join(", ")
|
||||
manifest.metadata.album = resolvedManifest.metadata.album.title
|
||||
|
||||
const coverUID = resolvedManifest.metadata.album.cover.replace(/-/g, "/")
|
||||
|
||||
manifest.metadata.cover = `https://resources.tidal.com/images/${coverUID}/1280x1280.jpg`
|
||||
|
||||
return manifest
|
||||
}
|
||||
}
|
@ -8,6 +8,8 @@ import SeekBar from "components/Player/SeekBar"
|
||||
import Controls from "components/Player/Controls"
|
||||
import { WithPlayerContext, Context } from "contexts/WithPlayerContext"
|
||||
|
||||
import ExtraActions from "components/Player/ExtraActions"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
const ServiceIndicator = (props) => {
|
||||
@ -112,18 +114,7 @@ const AudioPlayerComponent = (props) => {
|
||||
disabled={ctx.control_locked}
|
||||
/>
|
||||
|
||||
<div className="extra_actions">
|
||||
<Button
|
||||
type="ghost"
|
||||
icon={<Icons.MdLyrics />}
|
||||
disabled={!lyricsEnabled}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="ghost"
|
||||
icon={<Icons.MdQueueMusic />}
|
||||
/>
|
||||
</div>
|
||||
<ExtraActions />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
@ -506,21 +506,41 @@ export default class MusicModel {
|
||||
/**
|
||||
* Toggles the like status of a track.
|
||||
*
|
||||
* @param {number} track_id - The ID of the track.
|
||||
* @throws {Error} If track_id is not provided.
|
||||
* @return {Promise<Object>} The response data.
|
||||
* @param {Object} manifest - The manifest object containing track information.
|
||||
* @param {boolean} to - The like status to toggle (true for like, false for unlike).
|
||||
* @throws {Error} Throws an error if the manifest is missing.
|
||||
* @return {Object} The response data from the API.
|
||||
*/
|
||||
static async toggleTrackLike(track_id) {
|
||||
if (!track_id) {
|
||||
throw new Error("Track ID is required")
|
||||
static async toggleTrackLike(manifest, to) {
|
||||
if (!manifest) {
|
||||
throw new Error("Manifest is required")
|
||||
}
|
||||
|
||||
const response = await request({
|
||||
instance: MusicModel.api_instance,
|
||||
method: "POST",
|
||||
url: `/tracks/${track_id}/toggle-like`,
|
||||
})
|
||||
console.log(`Toggling track ${manifest._id} like status to ${to}`)
|
||||
|
||||
return response.data
|
||||
const track_id = manifest._id
|
||||
|
||||
switch (manifest.service) {
|
||||
case "tidal": {
|
||||
const response = await SyncModel.tidalCore.toggleTrackLike({
|
||||
track_id,
|
||||
to,
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
default: {
|
||||
const response = await request({
|
||||
instance: MusicModel.api_instance,
|
||||
method: to ? "POST" : "DELETE",
|
||||
url: `/tracks/${track_id}/like`,
|
||||
params: {
|
||||
service: manifest.service
|
||||
}
|
||||
})
|
||||
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -156,4 +156,17 @@ export default class TidalService {
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
static async toggleTrackLike({
|
||||
track_id,
|
||||
to,
|
||||
}) {
|
||||
const { data } = await request({
|
||||
instance: TidalService.api_instance,
|
||||
method: to ? "POST" : "DELETE",
|
||||
url: `/services/tidal/track/${track_id}/like`,
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
}
|
@ -21,27 +21,16 @@ export default async (req, res) => {
|
||||
user_id: req.session.user_id,
|
||||
})
|
||||
|
||||
if (like) {
|
||||
await like.delete()
|
||||
like = null
|
||||
} else {
|
||||
like = new TrackLike({
|
||||
track_id: track_id,
|
||||
user_id: req.session.user_id,
|
||||
created_at: new Date().getTime(),
|
||||
})
|
||||
|
||||
await like.save()
|
||||
}
|
||||
await like.delete()
|
||||
|
||||
global.ws.io.emit("music:self:track:toggle:like", {
|
||||
track_id: track_id,
|
||||
user_id: req.session.user_id,
|
||||
action: like ? "liked" : "unliked",
|
||||
action: "unliked",
|
||||
})
|
||||
|
||||
return res.status(200).json({
|
||||
message: "ok",
|
||||
action: like ? "liked" : "unliked",
|
||||
action: "unliked",
|
||||
})
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import { TrackLike, Track } from "@shared-classes/DbModels"
|
||||
import { AuthorizationError, NotFoundError } from "@shared-classes/Errors"
|
||||
|
||||
export default async (req, res) => {
|
||||
if (!req.session) {
|
||||
return new AuthorizationError(req, res)
|
||||
}
|
||||
|
||||
const { track_id } = req.params
|
||||
|
||||
const track = await Track.findById(track_id).catch((err) => {
|
||||
return null
|
||||
})
|
||||
|
||||
if (!track) {
|
||||
return new NotFoundError(req, res, "Track not found")
|
||||
}
|
||||
|
||||
let like = await TrackLike.findOne({
|
||||
track_id: track_id,
|
||||
user_id: req.session.user_id,
|
||||
})
|
||||
|
||||
like = new TrackLike({
|
||||
track_id: track_id,
|
||||
user_id: req.session.user_id,
|
||||
created_at: new Date().getTime(),
|
||||
})
|
||||
|
||||
await like.save()
|
||||
|
||||
global.ws.io.emit("music:self:track:toggle:like", {
|
||||
track_id: track_id,
|
||||
user_id: req.session.user_id,
|
||||
action: "liked" ,
|
||||
})
|
||||
|
||||
return res.status(200).json({
|
||||
message: "ok",
|
||||
action: "liked",
|
||||
})
|
||||
}
|
@ -49,6 +49,7 @@
|
||||
"normalize-url": "^8.0.0",
|
||||
"p-map": "^6.0.0",
|
||||
"p-queue": "^7.3.4",
|
||||
"qs": "^6.11.2",
|
||||
"redis": "^4.6.6",
|
||||
"sharp": "^0.31.3",
|
||||
"split-chunk-merge": "^1.0.0",
|
||||
@ -61,4 +62,4 @@
|
||||
"mocha": "^10.2.0",
|
||||
"nodemon": "^2.0.15"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,34 @@
|
||||
import SecureSyncEntry from "@shared-classes/SecureSyncEntry"
|
||||
import { AuthorizationError, InternalServerError } from "@shared-classes/Errors"
|
||||
|
||||
import TidalAPI from "@shared-classes/TidalAPI"
|
||||
|
||||
export default async (req, res) => {
|
||||
if (!req.session) {
|
||||
return new AuthorizationError(req, res)
|
||||
}
|
||||
|
||||
try {
|
||||
const access_token = await SecureSyncEntry.get(req.session.user_id.toString(), "tidal_access_token")
|
||||
|
||||
if (!access_token) {
|
||||
return new AuthorizationError(req, res, "Its needed to link your TIDAL account to perform this action.")
|
||||
}
|
||||
|
||||
let user_data = await SecureSyncEntry.get(req.session.user_id.toString(), "tidal_user")
|
||||
|
||||
user_data = JSON.parse(user_data)
|
||||
|
||||
let response = await TidalAPI.toggleTrackLike({
|
||||
trackId: req.params.track_id,
|
||||
to: false,
|
||||
user_id: user_data.id,
|
||||
access_token,
|
||||
country: user_data.country,
|
||||
})
|
||||
|
||||
return res.json(response)
|
||||
} catch (error) {
|
||||
return new InternalServerError(req, res, error)
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
import SecureSyncEntry from "@shared-classes/SecureSyncEntry"
|
||||
import { AuthorizationError, InternalServerError } from "@shared-classes/Errors"
|
||||
|
||||
import TidalAPI from "@shared-classes/TidalAPI"
|
||||
|
||||
export default async (req, res) => {
|
||||
if (!req.session) {
|
||||
return new AuthorizationError(req, res)
|
||||
}
|
||||
|
||||
try {
|
||||
const access_token = await SecureSyncEntry.get(req.session.user_id.toString(), "tidal_access_token")
|
||||
|
||||
if (!access_token) {
|
||||
return new AuthorizationError(req, res, "Its needed to link your TIDAL account to perform this action.")
|
||||
}
|
||||
|
||||
let user_data = await SecureSyncEntry.get(req.session.user_id.toString(), "tidal_user")
|
||||
|
||||
user_data = JSON.parse(user_data)
|
||||
|
||||
let response = await TidalAPI.toggleTrackLike({
|
||||
trackId: req.params.track_id,
|
||||
to: true,
|
||||
user_id: user_data.id,
|
||||
access_token,
|
||||
country: user_data.countryCode,
|
||||
})
|
||||
|
||||
return res.json(response)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return new InternalServerError(req, res, error)
|
||||
}
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
import axios from "axios"
|
||||
import FormData from "form-data"
|
||||
import qs from "qs"
|
||||
|
||||
const TIDAL_CLIENT_ID = process.env.TIDAL_CLIENT_ID
|
||||
const TIDAL_CLIENT_SECRET = process.env.TIDAL_CLIENT_SECRET
|
||||
@ -256,7 +258,6 @@ export default class TidalAPI {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves self favorite playlists based on specified parameters.
|
||||
*
|
||||
@ -423,11 +424,58 @@ export default class TidalAPI {
|
||||
return response.data
|
||||
}
|
||||
|
||||
static async toggleTrackLike(track_id) {
|
||||
/**
|
||||
* Toggles the like status of a track.
|
||||
*
|
||||
* @param {Object} params - The parameters for toggling the track like.
|
||||
* @param {string} params.trackId - The ID of the track to toggle the like status.
|
||||
* @param {boolean} params.to - The new like status. True to like the track, false to unlike it.
|
||||
* @param {string} params.user_id - The ID of the user performing the action.
|
||||
* @param {string} params.access_token - The access token for authentication.
|
||||
* @param {string} params.country - The country code.
|
||||
* @return {Object} - The response data from the API.
|
||||
*/
|
||||
static async toggleTrackLike({
|
||||
trackId,
|
||||
to,
|
||||
user_id,
|
||||
|
||||
access_token,
|
||||
country,
|
||||
}) {
|
||||
let url = `${TidalAPI.API_V1}/users/${user_id}/favorites/tracks`
|
||||
let payload = null
|
||||
let headers = {
|
||||
Origin: "http://listen.tidal.com",
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
}
|
||||
|
||||
if (!to) {
|
||||
url = `${url}/${trackId}`
|
||||
} else {
|
||||
payload = qs.stringify({
|
||||
trackIds: trackId,
|
||||
onArtifactNotFound: "FAIL"
|
||||
})
|
||||
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
}
|
||||
|
||||
let response = await axios({
|
||||
url: url,
|
||||
method: to ? "POST" : "DELETE",
|
||||
headers: headers,
|
||||
params: {
|
||||
countryCode: country,
|
||||
deviceType: "BROWSER"
|
||||
},
|
||||
data: payload
|
||||
})
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
||||
static async togglePlaylistLike(playlist_id) {
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user