mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 18:44:16 +00:00
progressive loading for favorite tracks
This commit is contained in:
parent
8e686e1e4e
commit
7c448c854f
@ -92,6 +92,7 @@
|
|||||||
"react-helmet": "6.1.0",
|
"react-helmet": "6.1.0",
|
||||||
"react-i18next": "11.15.3",
|
"react-i18next": "11.15.3",
|
||||||
"react-icons": "^4.8.0",
|
"react-icons": "^4.8.0",
|
||||||
|
"react-infinite-scroll-component": "^6.1.0",
|
||||||
"react-inlinesvg": "^3.0.1",
|
"react-inlinesvg": "^3.0.1",
|
||||||
"react-intersection-observer": "8.33.1",
|
"react-intersection-observer": "8.33.1",
|
||||||
"react-json-view": "1.21.3",
|
"react-json-view": "1.21.3",
|
||||||
|
@ -9,6 +9,8 @@ import useWsEvents from "hooks/useWsEvents"
|
|||||||
|
|
||||||
import { WithPlayerContext } from "contexts/WithPlayerContext"
|
import { WithPlayerContext } from "contexts/WithPlayerContext"
|
||||||
|
|
||||||
|
import LoadMore from "components/LoadMore"
|
||||||
|
|
||||||
import { ImageViewer } from "components"
|
import { ImageViewer } from "components"
|
||||||
import { Icons } from "components/Icons"
|
import { Icons } from "components/Icons"
|
||||||
|
|
||||||
@ -67,17 +69,6 @@ export default (props) => {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const returnTracks = (list) => {
|
|
||||||
return list.map((item, index) => {
|
|
||||||
return <MusicTrack
|
|
||||||
order={index + 1}
|
|
||||||
track={item}
|
|
||||||
onClickPlayBtn={() => handleOnClickTrack(item)}
|
|
||||||
onLike={() => handleTrackLike(item)}
|
|
||||||
/>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleOnSearchChange = (value) => {
|
const handleOnSearchChange = (value) => {
|
||||||
debounceSearch = setTimeout(() => {
|
debounceSearch = setTimeout(() => {
|
||||||
makeSearch(value)
|
makeSearch(value)
|
||||||
@ -120,6 +111,10 @@ export default (props) => {
|
|||||||
socketName: "music",
|
socketName: "music",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setPlaylist(props.playlist)
|
||||||
|
}, [props.playlist])
|
||||||
|
|
||||||
if (!playlist) {
|
if (!playlist) {
|
||||||
return <antd.Skeleton active />
|
return <antd.Skeleton active />
|
||||||
}
|
}
|
||||||
@ -164,7 +159,7 @@ export default (props) => {
|
|||||||
}
|
}
|
||||||
<div className="play_info_statistics_item">
|
<div className="play_info_statistics_item">
|
||||||
<p>
|
<p>
|
||||||
<Icons.MdLibraryMusic /> {playlist.list.length} Tracks
|
<Icons.MdLibraryMusic /> {props.length ?? playlist.list.length} Tracks
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -192,11 +187,36 @@ export default (props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<WithPlayerContext>
|
{
|
||||||
{
|
playlist.list.length === 0 && <antd.Empty
|
||||||
returnTracks(searchResults ?? playlist.list)
|
description={
|
||||||
}
|
<>
|
||||||
</WithPlayerContext>
|
<Icons.MdLibraryMusic /> This playlist its empty!
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
playlist.list.length > 0 && <LoadMore
|
||||||
|
className="list_content"
|
||||||
|
loadingComponent={() => <antd.Skeleton />}
|
||||||
|
onBottom={props.onLoadMore}
|
||||||
|
hasMore={props.hasMore}
|
||||||
|
>
|
||||||
|
<WithPlayerContext>
|
||||||
|
{
|
||||||
|
playlist.list.map((item, index) => {
|
||||||
|
return <MusicTrack
|
||||||
|
order={index + 1}
|
||||||
|
track={item}
|
||||||
|
onClickPlayBtn={() => handleOnClickTrack(item)}
|
||||||
|
onLike={() => handleTrackLike(item)}
|
||||||
|
/>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</WithPlayerContext>
|
||||||
|
</LoadMore>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
@ -239,5 +239,12 @@ html {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list_content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,30 +5,124 @@ import PlaylistView from "components/Music/PlaylistView"
|
|||||||
|
|
||||||
import MusicModel from "models/music"
|
import MusicModel from "models/music"
|
||||||
|
|
||||||
export default () => {
|
export default class FavoriteTracks extends React.Component {
|
||||||
const [L_Favorites, R_Favorites, E_Favorites] = app.cores.api.useRequest(MusicModel.getFavorites, {
|
state = {
|
||||||
useTidal: app.cores.sync.getActiveLinkedServices().tidal
|
error: null,
|
||||||
})
|
|
||||||
|
|
||||||
if (E_Favorites) {
|
initialLoading: true,
|
||||||
return <antd.Result
|
loading: false,
|
||||||
status="error"
|
|
||||||
title="Error"
|
list: [],
|
||||||
subTitle={E_Favorites.message}
|
total_length: 0,
|
||||||
|
|
||||||
|
empty: false,
|
||||||
|
hasMore: true,
|
||||||
|
offset: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
static loadLimit = 50
|
||||||
|
|
||||||
|
componentDidMount = async () => {
|
||||||
|
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 <antd.Result
|
||||||
|
status="error"
|
||||||
|
title="Error"
|
||||||
|
subTitle={this.state.error}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.initialLoading) {
|
||||||
|
return <antd.Skeleton active />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <PlaylistView
|
||||||
|
type="vertical"
|
||||||
|
playlist={{
|
||||||
|
title: "Your favorites",
|
||||||
|
cover: "https://storage.ragestudio.net/comty-static-assets/favorite_song.png",
|
||||||
|
list: this.state.list
|
||||||
|
}}
|
||||||
|
centered={app.isMobile}
|
||||||
|
onLoadMore={this.onLoadMore}
|
||||||
|
hasMore={this.state.hasMore}
|
||||||
|
empty={this.state.empty}
|
||||||
|
length={this.state.total_length}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (L_Favorites) {
|
|
||||||
return <antd.Skeleton active />
|
|
||||||
}
|
|
||||||
|
|
||||||
return <PlaylistView
|
|
||||||
type="vertical"
|
|
||||||
playlist={{
|
|
||||||
title: "Your favorites",
|
|
||||||
cover: "https://storage.ragestudio.net/comty-static-assets/favorite_song.png",
|
|
||||||
list: R_Favorites
|
|
||||||
}}
|
|
||||||
centered={app.isMobile}
|
|
||||||
/>
|
|
||||||
}
|
}
|
@ -7,32 +7,47 @@ export default class MusicModel {
|
|||||||
return globalThis.__comty_shared_state.instances["music"]
|
return globalThis.__comty_shared_state.instances["music"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Move external services fetching to API
|
||||||
static getFavorites = async ({
|
static getFavorites = async ({
|
||||||
useTidal = false
|
useTidal = false,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
}) => {
|
}) => {
|
||||||
let result = []
|
let result = []
|
||||||
|
|
||||||
|
let limitPerRequesters = limit
|
||||||
|
|
||||||
|
if (useTidal) {
|
||||||
|
limitPerRequesters = limitPerRequesters / 2
|
||||||
|
}
|
||||||
|
|
||||||
const requesters = [
|
const requesters = [
|
||||||
async () => {
|
async () => {
|
||||||
let { data } = await request({
|
let { data } = await request({
|
||||||
instance: MusicModel.api_instance,
|
instance: MusicModel.api_instance,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: `/tracks/liked`,
|
url: `/tracks/liked`,
|
||||||
|
params: {
|
||||||
|
limit: limitPerRequesters,
|
||||||
|
offset,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
]
|
async () => {
|
||||||
|
if (!useTidal) {
|
||||||
if (useTidal) {
|
return []
|
||||||
requesters.push(
|
|
||||||
async () => {
|
|
||||||
const tidalResult = await SyncModel.tidalCore.getMyFavoriteTracks()
|
|
||||||
|
|
||||||
return tidalResult
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
const tidalResult = await SyncModel.tidalCore.getMyFavoriteTracks({
|
||||||
|
limit: limitPerRequesters,
|
||||||
|
offset,
|
||||||
|
})
|
||||||
|
|
||||||
|
return tidalResult
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
result = await pmap(
|
result = await pmap(
|
||||||
requesters,
|
requesters,
|
||||||
@ -46,17 +61,24 @@ export default class MusicModel {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
result = result.reduce((acc, cur) => {
|
let total_length = 0
|
||||||
return [...acc, ...cur]
|
|
||||||
|
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
|
return b.liked_at - a.liked_at
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(result)
|
return {
|
||||||
|
total_length,
|
||||||
return result
|
tracks,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static search = async (keywords, {
|
static search = async (keywords, {
|
||||||
|
@ -81,11 +81,18 @@ export default class TidalService {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getMyFavoriteTracks() {
|
static async getMyFavoriteTracks({
|
||||||
|
limit = 50,
|
||||||
|
offset = 0,
|
||||||
|
} = {}) {
|
||||||
const { data } = await request({
|
const { data } = await request({
|
||||||
instance: TidalService.api_instance,
|
instance: TidalService.api_instance,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: `/services/tidal/favorites/tracks`,
|
url: `/services/tidal/favorites/tracks`,
|
||||||
|
params: {
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
@ -1,14 +1,23 @@
|
|||||||
import { Track, TrackLike } from "@shared-classes/DbModels"
|
import { Track, TrackLike } from "@shared-classes/DbModels"
|
||||||
import { AuthorizationError } from "@shared-classes/Errors"
|
import { AuthorizationError } from "@shared-classes/Errors"
|
||||||
|
|
||||||
|
// TODO: Fetch from external linked services (like tidal, spotify, ...)
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
if (!req.session) {
|
if (!req.session) {
|
||||||
return new AuthorizationError(req, res)
|
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({
|
let likedTracks = await TrackLike.find({
|
||||||
user_id: req.session.user_id,
|
user_id: req.session.user_id,
|
||||||
})
|
})
|
||||||
|
.limit(Number(limit))
|
||||||
|
.skip(Number(offset))
|
||||||
.sort({ created_at: -1 })
|
.sort({ created_at: -1 })
|
||||||
|
|
||||||
const likedTracksIds = likedTracks.map((item) => {
|
const likedTracksIds = likedTracks.map((item) => {
|
||||||
@ -44,5 +53,8 @@ export default async (req, res) => {
|
|||||||
return indexA - indexB
|
return indexA - indexB
|
||||||
})
|
})
|
||||||
|
|
||||||
return res.json(tracks)
|
return res.json({
|
||||||
|
total_length: totalLikedTracks,
|
||||||
|
tracks,
|
||||||
|
})
|
||||||
}
|
}
|
@ -23,6 +23,8 @@ export default async (req, res) => {
|
|||||||
user_id: user_data.id,
|
user_id: user_data.id,
|
||||||
country: user_data.countryCode,
|
country: user_data.countryCode,
|
||||||
access_token: access_token,
|
access_token: access_token,
|
||||||
|
limit: Number(req.query.limit ?? 50),
|
||||||
|
offset: Number(req.query.offset ?? 0),
|
||||||
})
|
})
|
||||||
|
|
||||||
return res.json(response)
|
return res.json(response)
|
||||||
|
@ -196,6 +196,8 @@ export default class TidalAPI {
|
|||||||
user_id,
|
user_id,
|
||||||
country,
|
country,
|
||||||
access_token,
|
access_token,
|
||||||
|
limit = 100,
|
||||||
|
offset = 0,
|
||||||
}) {
|
}) {
|
||||||
const url = `https://api.tidal.com/v1/users/${user_id}/favorites/tracks?countryCode=${country}`
|
const url = `https://api.tidal.com/v1/users/${user_id}/favorites/tracks?countryCode=${country}`
|
||||||
|
|
||||||
@ -208,11 +210,13 @@ export default class TidalAPI {
|
|||||||
},
|
},
|
||||||
params: {
|
params: {
|
||||||
order: "DATE",
|
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
|
// get js time
|
||||||
item.item.liked_at = new Date(item.created).getTime()
|
item.item.liked_at = new Date(item.created).getTime()
|
||||||
item.item.service = "tidal"
|
item.item.service = "tidal"
|
||||||
@ -238,5 +242,10 @@ export default class TidalAPI {
|
|||||||
|
|
||||||
return item.item
|
return item.item
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
total_length: response.data.totalNumberOfItems,
|
||||||
|
tracks: response.data.items
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user