Skip to content
This repository has been archived by the owner on Oct 4, 2023. It is now read-only.

Commit

Permalink
[C-2842] Improve playlist image generation (#3762)
Browse files Browse the repository at this point in the history
  • Loading branch information
dylanjeffers authored Jul 19, 2023
1 parent 9c5a412 commit 9324a06
Show file tree
Hide file tree
Showing 20 changed files with 347 additions and 406 deletions.
62 changes: 26 additions & 36 deletions packages/common/src/hooks/useGeneratePlaylistArtwork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,41 @@ import { useCallback } from 'react'
import { useSelector } from 'react-redux'

import { ID } from 'models/Identifiers'
import { SquareSizes } from 'models/ImageSizes'
import { useAppContext } from 'src/context'
import { getCollection } from 'store/cache/collections/selectors'
import { getTrack } from 'store/cache/tracks/selectors'
import {
getCollection,
getCollectionTracks
} from 'store/cache/collections/selectors'
import { CommonState } from 'store/index'
import { removeNullable } from 'utils/typeUtils'
import { updatePlaylistArtwork } from 'utils/updatePlaylistArtwork'

export const useGeneratePlaylistArtwork = (collectionId: ID) => {
const collection = useSelector(
(state: CommonState) => getCollection(state, { id: collectionId })!
const collection = useSelector((state: CommonState) =>
getCollection(state, { id: collectionId })
)

const collectionTracks = useSelector((state: CommonState) => {
const trackIds = collection.playlist_contents.track_ids.map(
({ track }) => track
)
const tracks = trackIds
.map((trackId) => getTrack(state, { id: trackId }))
.filter(removeNullable)
.slice(0, 4)

if (tracks.length < 4) {
return tracks.slice(0, 1)
}

return tracks
})
const collectionTracks = useSelector((state: CommonState) =>
getCollectionTracks(state, { id: collectionId })
)

const { imageUtils, audiusBackend } = useAppContext()

return useCallback(async () => {
if (collectionTracks.length === 0) {
return { url: '', file: undefined }
}

const trackArtworkUrls = await Promise.all(
collectionTracks.map(async (track) => {
const { cover_art_sizes, cover_art } = track
return await audiusBackend.getImageUrl(
cover_art_sizes || cover_art,
SquareSizes.SIZE_1000_BY_1000
)
})
if (!collection || !collectionTracks) return null
const { artwork } = await updatePlaylistArtwork(
collection,
collectionTracks,
{ regenerate: true },
{ audiusBackend, generateImage: imageUtils.generatePlaylistArtwork }
)

return await imageUtils.generatePlaylistArtwork(trackArtworkUrls)
}, [collectionTracks, audiusBackend, imageUtils])
if (!artwork) return null
const { url, file } = artwork
if (!url) return null
return { url, file }
}, [
collection,
collectionTracks,
audiusBackend,
imageUtils.generatePlaylistArtwork
])
}
26 changes: 22 additions & 4 deletions packages/common/src/store/cache/collections/selectors.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { getAllEntries, getEntry } from 'store/cache/selectors'
import { getTracks } from 'store/cache/tracks/selectors'
import { getTrack, getTracks } from 'store/cache/tracks/selectors'
import { getUser as getUserById, getUsers } from 'store/cache/users/selectors'
import type { CommonState } from 'store/commonStore'
import { removeNullable } from 'utils/typeUtils'
import { Uid } from 'utils/uid'

import type { ID, UID, Collection, User } from '../../../models'
Expand Down Expand Up @@ -69,7 +70,24 @@ export const getCollectionsByUid = (state: CommonState) => {
}, {} as { [uid: string]: Collection | null })
}

const getCollectionTracks = (state: CommonState, { id }: { id?: ID }) => {
export const getCollectionTracks = (
state: CommonState,
{ id }: { id?: ID }
) => {
const collection = getCollection(state, { id })
if (!collection) return null

const trackIds = collection.playlist_contents.track_ids.map(
({ track }) => track
)
const tracks = trackIds
.map((trackId) => getTrack(state, { id: trackId }))
.filter(removeNullable)

return tracks
}

const getCollectionTracksMap = (state: CommonState, { id }: { id?: ID }) => {
const collection = getCollection(state, { id })
const collectionTrackIds = collection?.playlist_contents.track_ids.map(
(track_id) => track_id.track // track === actual track id, oof
Expand All @@ -81,7 +99,7 @@ export const getIsCollectionEmpty = (
state: CommonState,
{ id }: { id?: ID }
) => {
const collectionTracks = getCollectionTracks(state, { id })
const collectionTracks = getCollectionTracksMap(state, { id })

return Object.values(collectionTracks).length === 0
}
Expand All @@ -90,7 +108,7 @@ export const getCollecitonHasHiddenTracks = (
state: CommonState,
{ id }: { id?: ID }
) => {
const collectionTracks = getCollectionTracks(state, { id })
const collectionTracks = getCollectionTracksMap(state, { id })

return Object.values(collectionTracks)?.some((track) => track.is_unlisted)
}
Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/store/cache/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export const mergeCustomizer = (objValue: any, srcValue: any, key: string) => {
// Not every user request provides playlist_library,
// so always prefer it's existence, starting with latest
if (key === 'playlist_library') {
return objValue || srcValue
return srcValue || objValue
}

// Delete is unidirectional (after marked deleted, future updates are not reflected)
Expand Down
77 changes: 0 additions & 77 deletions packages/common/src/store/playlist-library/helpers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
addPlaylistToFolder,
extractTempPlaylistsFromLibrary,
replaceTempWithResolvedPlaylists,
removePlaylistLibraryTempPlaylists,
getPlaylistsNotInLibrary
} from './helpers'

Expand Down Expand Up @@ -478,82 +477,6 @@ describe('removePlaylistLibraryDuplicates', () => {
})
})

describe('removePlaylistLibraryTempPlaylists', () => {
it('can remove temporary playlists', () => {
const library = {
contents: [
{ type: 'temp_playlist', playlist_id: '33' },
{ type: 'explore_playlist', playlist_id: 'Heavy Rotation' },
{
type: 'folder',
name: 'My folder',
id: 'uuid',
contents: [
{ type: 'temp_playlist', playlist_id: '44' },
{ type: 'playlist', playlist_id: 5 },
{ type: 'playlist', playlist_id: 6 }
]
},
{ type: 'playlist', playlist_id: 3 },
{ type: 'playlist', playlist_id: 1 }
]
}
const ret = removePlaylistLibraryTempPlaylists(library)
expect(ret).toEqual({
contents: [
{ type: 'explore_playlist', playlist_id: 'Heavy Rotation' },
{
type: 'folder',
name: 'My folder',
id: 'uuid',
contents: [
{ type: 'playlist', playlist_id: 5 },
{ type: 'playlist', playlist_id: 6 }
]
},
{ type: 'playlist', playlist_id: 3 },
{ type: 'playlist', playlist_id: 1 }
]
})
})

it('does not remove non temp playlists', () => {
const library = {
contents: [
{ type: 'playlist', playlist_id: 1 },
{ type: 'playlist', playlist_id: 2 },
{ type: 'explore_playlist', playlist_id: 'Heavy Rotation' },
{
type: 'folder',
name: 'My folder',
id: 'uuid',
contents: [
{ type: 'playlist', playlist_id: 10 },
{ type: 'playlist', playlist_id: 11 }
]
},
{ type: 'playlist', playlist_id: 4 },
{ type: 'playlist', playlist_id: 5 },
{ type: 'playlist', playlist_id: 6 }
]
}
const ret = removePlaylistLibraryTempPlaylists(library)
expect(ret).toEqual(library)
})

it('can deal with empty library', () => {
const library = {
contents: []
}
let ret = removePlaylistLibraryTempPlaylists(library)
expect(ret).toEqual(library)

library.contents = null
ret = removePlaylistLibraryTempPlaylists(library)
expect(ret).toEqual(library)
})
})

describe('reorderPlaylistLibrary', () => {
it('can reorder adjacent playlists', () => {
const library = {
Expand Down
33 changes: 0 additions & 33 deletions packages/common/src/store/playlist-library/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,39 +314,6 @@ export const addFolderToLibrary = (
}
}

/**
* Removes temp playlists from playlist library (without mutating)
* @param library
* @returns a copy of the library with all temp playlists removed
*/
export const removePlaylistLibraryTempPlaylists = (
library: PlaylistLibrary | PlaylistLibraryFolder
) => {
if (!library.contents) return library
const newContents: (PlaylistLibraryFolder | PlaylistLibraryIdentifier)[] = []
for (const item of library.contents) {
switch (item.type) {
case 'folder': {
const folder = removePlaylistLibraryTempPlaylists(
item
) as PlaylistLibraryFolder
newContents.push(folder)
break
}
case 'temp_playlist':
break
case 'explore_playlist':
case 'playlist':
newContents.push(item)
break
}
}
return {
...library,
contents: newContents
}
}

/**
* Removes duplicates in a playlist library
* @param library
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ export * from './twitter'
export * from './streaming'
export * from './dogEarUtils'
export * from './uploadConstants'
export * from './updatePlaylistArtwork'
99 changes: 99 additions & 0 deletions packages/common/src/utils/updatePlaylistArtwork.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { isEqual } from 'lodash'

import { Collection } from 'models/Collection'
import { SquareSizes } from 'models/ImageSizes'
import { Track } from 'models/Track'
import { AudiusBackend } from 'services/audius-backend'

import { Nullable } from './typeUtils'

const filterTrack = (track1: Track) => (track2: Track) =>
track1.track_id === track2.track_id

type ArtworkActions = {
added?: Track
removed?: Track
reordered?: Track[]
updated?: Track[]
regenerate?: boolean
}

type Context = {
audiusBackend: AudiusBackend
generateImage: (urls: string[]) => Promise<{ url: string; file: File }>
}

export const updatePlaylistArtwork = async (
collection: Collection,
tracks: Track[],
actions: ArtworkActions,
context: Context
) => {
const { is_image_autogenerated } = collection
if (!is_image_autogenerated && !actions.regenerate) {
return collection
}

let tracksForImage: Nullable<Track[]> = null

if (actions.added) {
if (tracks.length === 0) {
tracksForImage = [actions.added]
} else if (tracks.length === 3) {
tracksForImage = [...tracks, actions.added]
}
} else if (actions.removed) {
const { removed } = actions
const removedIndex = tracks.findIndex(
(track) => track.track_id === removed.track_id
)
if (removedIndex === -1) {
// continue
} else if (tracks.length >= 4 && removedIndex < 4) {
tracksForImage = tracks.filter(filterTrack(removed)).slice(0, 4)
} else if (tracks.length === 4) {
tracksForImage = tracks.filter(filterTrack(removed)).slice(0, 1)
} else if (tracks.length < 4 && removedIndex === 0) {
tracksForImage = tracks.slice(1, 2)
}
} else if (actions.reordered) {
if (
tracks.length >= 4 &&
!isEqual(actions.reordered.slice(0, 4), tracks.slice(0, 4))
) {
tracksForImage = actions.reordered.slice(0, 4)
} else if (tracks.length < 4 && !isEqual(actions.reordered[0], tracks[0])) {
tracksForImage = [actions.reordered[0]]
}
} else if (actions.updated) {
if (actions.updated.length < 4 && tracks.length < 4) {
if (!isEqual(actions.updated[0], tracks[0])) {
tracksForImage = [actions.updated[0]]
}
} else if (actions.updated.length < 4 && tracks.length >= 4) {
tracksForImage = [actions.updated[0]]
} else if (!isEqual(actions.updated.slice(0, 4), tracks.slice(0, 4))) {
tracksForImage = actions.updated.slice(0, 4)
}
} else if (actions.regenerate) {
tracksForImage = tracks.slice(0, 4)
}

if (tracksForImage) {
const trackUrls = await Promise.all(
tracksForImage.map(async (track) => {
const { cover_art_sizes, cover_art } = track
return await context.audiusBackend.getImageUrl(
cover_art_sizes ?? cover_art,
SquareSizes.SIZE_1000_BY_1000
)
})
)

const artwork = await context.generateImage(trackUrls)
collection.artwork = artwork
collection.is_image_autogenerated = true
}

return collection
}
6 changes: 5 additions & 1 deletion packages/mobile/src/hooks/useContentNodeImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ export const createAllImageSources = ({
return []
}

if (cid.startsWith('data:image') || cid.startsWith('file://')) {
if (
cid.startsWith('data:image') ||
cid.startsWith('file://') ||
cid.startsWith('/')
) {
return [...(localSource ? [localSource] : []), { uri: cid }]
}

Expand Down
Loading

0 comments on commit 9324a06

Please sign in to comment.