diff --git a/packages/app/package.json b/packages/app/package.json index a7b728b6..e20c74a5 100755 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -92,6 +92,7 @@ "react-helmet": "6.1.0", "react-i18next": "11.15.3", "react-icons": "^4.8.0", + "react-infinite-scroll-component": "^6.1.0", "react-inlinesvg": "^3.0.1", "react-intersection-observer": "8.33.1", "react-json-view": "1.21.3", @@ -174,4 +175,4 @@ ] } } -} \ No newline at end of file +} diff --git a/packages/app/src/components/Music/PlaylistView/index.jsx b/packages/app/src/components/Music/PlaylistView/index.jsx index aa4a02a2..b341c8cd 100644 --- a/packages/app/src/components/Music/PlaylistView/index.jsx +++ b/packages/app/src/components/Music/PlaylistView/index.jsx @@ -9,6 +9,8 @@ import useWsEvents from "hooks/useWsEvents" import { WithPlayerContext } from "contexts/WithPlayerContext" +import LoadMore from "components/LoadMore" + import { ImageViewer } from "components" import { Icons } from "components/Icons" @@ -67,17 +69,6 @@ export default (props) => { })) } - const returnTracks = (list) => { - return list.map((item, index) => { - return handleOnClickTrack(item)} - onLike={() => handleTrackLike(item)} - /> - }) - } - const handleOnSearchChange = (value) => { debounceSearch = setTimeout(() => { makeSearch(value) @@ -120,6 +111,10 @@ export default (props) => { socketName: "music", }) + React.useEffect(() => { + setPlaylist(props.playlist) + }, [props.playlist]) + if (!playlist) { return } @@ -164,7 +159,7 @@ export default (props) => { }

- {playlist.list.length} Tracks + {props.length ?? playlist.list.length} Tracks

@@ -192,11 +187,36 @@ export default (props) => { /> - - { - returnTracks(searchResults ?? playlist.list) - } - + { + playlist.list.length === 0 && + This playlist its empty! + + } + /> + } + { + playlist.list.length > 0 && } + onBottom={props.onLoadMore} + hasMore={props.hasMore} + > + + { + playlist.list.map((item, index) => { + return handleOnClickTrack(item)} + onLike={() => handleTrackLike(item)} + /> + }) + } + + + } } \ No newline at end of file diff --git a/packages/app/src/components/Music/PlaylistView/index.less b/packages/app/src/components/Music/PlaylistView/index.less index a3842eed..9ce2ec64 100755 --- a/packages/app/src/components/Music/PlaylistView/index.less +++ b/packages/app/src/components/Music/PlaylistView/index.less @@ -239,5 +239,12 @@ html { align-items: center; justify-content: space-between; } + + .list_content { + display: flex; + flex-direction: column; + + gap: 10px; + } } } \ No newline at end of file diff --git a/packages/app/src/pages/music/components/favorites/index.jsx b/packages/app/src/pages/music/components/favorites/index.jsx index 73eb6e0f..7087209a 100644 --- a/packages/app/src/pages/music/components/favorites/index.jsx +++ b/packages/app/src/pages/music/components/favorites/index.jsx @@ -5,30 +5,124 @@ import PlaylistView from "components/Music/PlaylistView" import MusicModel from "models/music" -export default () => { - const [L_Favorites, R_Favorites, E_Favorites] = app.cores.api.useRequest(MusicModel.getFavorites, { - useTidal: app.cores.sync.getActiveLinkedServices().tidal - }) +export default class FavoriteTracks extends React.Component { + state = { + error: null, - if (E_Favorites) { - return { + await this.loadItems() + } + + onLoadMore = async () => { + console.log(`Loading more items...`, this.state.offset) + + const newOffset = this.state.offset + FavoriteTracks.loadLimit + + await this.setState({ + offset: newOffset, + }) + + await this.loadItems({ + offset: newOffset, + }) + } + + loadItems = async ({ + replace = false, + offset = 0, + limit = FavoriteTracks.loadLimit, + } = {}) => { + this.setState({ + loading: true, + }) + + const result = await MusicModel.getFavorites({ + useTidal: app.cores.sync.getActiveLinkedServices().tidal, + offset: offset, + limit: limit, + }).catch((err) => { + this.setState({ + error: err.message, + }) + return false + }) + + console.log("Loaded favorites => ", result) + + if (result) { + const { tracks, total_length } = result + + this.setState({ + total_length + }) + + if (tracks.length === 0) { + if (offset === 0) { + this.setState({ + empty: true, + }) + } + + return this.setState({ + hasMore: false, + }) + } + + if (replace) { + this.setState({ + list: tracks, + }) + } else { + this.setState({ + list: [...this.state.list, ...tracks], + }) + } + } + + this.setState({ + loading: false, + initialLoading: false, + }) + } + + render() { + if (this.state.error) { + return + } + + if (this.state.initialLoading) { + return + } + + return } - - if (L_Favorites) { - return - } - - return } \ No newline at end of file diff --git a/packages/comty.js/src/models/music/index.js b/packages/comty.js/src/models/music/index.js index 4c022b26..0acefd2a 100644 --- a/packages/comty.js/src/models/music/index.js +++ b/packages/comty.js/src/models/music/index.js @@ -7,32 +7,47 @@ export default class MusicModel { return globalThis.__comty_shared_state.instances["music"] } + // TODO: Move external services fetching to API static getFavorites = async ({ - useTidal = false + useTidal = false, + limit, + offset, }) => { let result = [] + let limitPerRequesters = limit + + if (useTidal) { + limitPerRequesters = limitPerRequesters / 2 + } + const requesters = [ async () => { let { data } = await request({ instance: MusicModel.api_instance, method: "GET", url: `/tracks/liked`, + params: { + limit: limitPerRequesters, + offset, + }, }) return data }, - ] - - if (useTidal) { - requesters.push( - async () => { - const tidalResult = await SyncModel.tidalCore.getMyFavoriteTracks() - - return tidalResult + async () => { + if (!useTidal) { + return [] } - ) - } + + const tidalResult = await SyncModel.tidalCore.getMyFavoriteTracks({ + limit: limitPerRequesters, + offset, + }) + + return tidalResult + }, + ] result = await pmap( requesters, @@ -46,17 +61,24 @@ export default class MusicModel { } ) - result = result.reduce((acc, cur) => { - return [...acc, ...cur] + let total_length = 0 + + result.forEach((result) => { + total_length += result.total_length + }) + + let tracks = result.reduce((acc, cur) => { + return [...acc, ...cur.tracks] }, []) - result = result.sort((a, b) => { + tracks = tracks.sort((a, b) => { return b.liked_at - a.liked_at }) - console.log(result) - - return result + return { + total_length, + tracks, + } } static search = async (keywords, { diff --git a/packages/comty.js/src/models/sync/services/tidal.js b/packages/comty.js/src/models/sync/services/tidal.js index d91cbd02..a2aeb1b3 100644 --- a/packages/comty.js/src/models/sync/services/tidal.js +++ b/packages/comty.js/src/models/sync/services/tidal.js @@ -81,11 +81,18 @@ export default class TidalService { return data } - static async getMyFavoriteTracks() { + static async getMyFavoriteTracks({ + limit = 50, + offset = 0, + } = {}) { const { data } = await request({ instance: TidalService.api_instance, method: "GET", url: `/services/tidal/favorites/tracks`, + params: { + limit, + offset, + }, }) return data diff --git a/packages/music_server/src/controllers/tracks/routes/get/liked.js b/packages/music_server/src/controllers/tracks/routes/get/liked.js index d834f1a9..4eda7bac 100644 --- a/packages/music_server/src/controllers/tracks/routes/get/liked.js +++ b/packages/music_server/src/controllers/tracks/routes/get/liked.js @@ -1,14 +1,23 @@ import { Track, TrackLike } from "@shared-classes/DbModels" import { AuthorizationError } from "@shared-classes/Errors" +// TODO: Fetch from external linked services (like tidal, spotify, ...) export default async (req, res) => { if (!req.session) { return new AuthorizationError(req, res) } + const { limit = 100, offset = 0 } = req.query + + let totalLikedTracks = await TrackLike.count({ + user_id: req.session.user_id, + }) + let likedTracks = await TrackLike.find({ user_id: req.session.user_id, }) + .limit(Number(limit)) + .skip(Number(offset)) .sort({ created_at: -1 }) const likedTracksIds = likedTracks.map((item) => { @@ -44,5 +53,8 @@ export default async (req, res) => { return indexA - indexB }) - return res.json(tracks) + return res.json({ + total_length: totalLikedTracks, + tracks, + }) } \ No newline at end of file diff --git a/packages/sync_server/src/controllers/services/routes/get/tidal/favorites/tracks.js b/packages/sync_server/src/controllers/services/routes/get/tidal/favorites/tracks.js index 2f43b1e5..b1925878 100644 --- a/packages/sync_server/src/controllers/services/routes/get/tidal/favorites/tracks.js +++ b/packages/sync_server/src/controllers/services/routes/get/tidal/favorites/tracks.js @@ -23,6 +23,8 @@ export default async (req, res) => { user_id: user_data.id, country: user_data.countryCode, access_token: access_token, + limit: Number(req.query.limit ?? 50), + offset: Number(req.query.offset ?? 0), }) return res.json(response) diff --git a/shared/classes/TidalAPI/index.js b/shared/classes/TidalAPI/index.js index 34e93480..a562f9ba 100644 --- a/shared/classes/TidalAPI/index.js +++ b/shared/classes/TidalAPI/index.js @@ -196,6 +196,8 @@ export default class TidalAPI { user_id, country, access_token, + limit = 100, + offset = 0, }) { const url = `https://api.tidal.com/v1/users/${user_id}/favorites/tracks?countryCode=${country}` @@ -208,11 +210,13 @@ export default class TidalAPI { }, params: { order: "DATE", - orderDirection: "DESC" + orderDirection: "DESC", + limit: limit, + offset: offset, } }) - return response.data.items.map((item) => { + response.data.items = response.data.items.map((item) => { // get js time item.item.liked_at = new Date(item.created).getTime() item.item.service = "tidal" @@ -238,5 +242,10 @@ export default class TidalAPI { return item.item }) + + return { + total_length: response.data.totalNumberOfItems, + tracks: response.data.items + } } } \ No newline at end of file