diff --git a/backend/package.json b/backend/package.json index ae1d756f..38447c2a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -50,7 +50,15 @@ "!src/app.js", "!src/constants/*.js", "!src/config/*.js" - ] + ], + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 100, + "statements": 100 + } + } }, "standard": { "parser": "babel-eslint", diff --git a/backend/src/app.js b/backend/src/app.js index 9327fef0..5f95b176 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -22,7 +22,7 @@ app.use(morgan('combined', { stream: logger.stream })) const server = http.createServer(app) const socketio = io(server, { pingTimeout: 30000 }) -const broadcastToAll = (key, message) => Broadcaster.toAllGeneric(socketio, key, message) +const broadcastToAll = (key, message) => Broadcaster.toAll(socketio, key, message) const broadcastMopidyStateChange = (online) => Broadcaster.toAllMopidy(socketio, { online }) const allowSocketConnections = (mopidy) => { Scheduler.scheduleAutoPlayback({ stop: () => mopidy.playback.stop() }) diff --git a/backend/src/constants/message.js b/backend/src/constants/message.js index 2d5f8d95..4804a8dd 100644 --- a/backend/src/constants/message.js +++ b/backend/src/constants/message.js @@ -2,6 +2,7 @@ export default { GENERIC: 'message', MOPIDY: 'mopidy', SEARCH: 'search', + IMAGE: 'image', INCOMING_CORE: 'INCOMING MOPIDY [CORE]', INCOMING_CLIENT: 'INCOMING CLIENT', INCOMING_MOPIDY: 'INCOMING MOPIDY', diff --git a/backend/src/handlers/mopidy/image-cache/index.js b/backend/src/handlers/mopidy/image-cache/index.js index 58036d16..9752f438 100644 --- a/backend/src/handlers/mopidy/image-cache/index.js +++ b/backend/src/handlers/mopidy/image-cache/index.js @@ -13,30 +13,20 @@ const expiresDate = (imgs) => { return new Date(today.getTime() + (day * 30)) } -const addToCacheHandler = (encodedKey) => { - const handler = (data) => { - const uri = encodedKey.split('#')[1] - - return Image.create({ - expireAt: expiresDate(data[uri]), - uri: encodedKey, - data - }) - } - return handler -} +const storeImage = (uri, data) => Image.create({ expireAt: expiresDate(data[uri]), uri, data }) +const addToCacheHandler = (uri) => (data) => storeImage(uri, data) const ImageCache = { check: (key, data) => new Promise((resolve) => { if (!isImageKey(key)) return resolve({ image: false }) - const encodedKey = `${key}#${data[0][0]}` + const uri = data[0][0] - fetchFromCache(encodedKey).then((response) => { + fetchFromCache(uri).then((response) => { if (response) { - logger.info('Using cache', { key: encodedKey }) + logger.info('FOUND CACHED IMAGE', { key: uri }) return resolve({ image: response.data }) } else { - return resolve({ addToCache: addToCacheHandler(encodedKey) }) + return resolve({ addToCache: addToCacheHandler(uri) }) } }) }) diff --git a/backend/src/handlers/mopidy/image-cache/index.spec.js b/backend/src/handlers/mopidy/image-cache/index.spec.js index 98a3fc11..bb858a7b 100644 --- a/backend/src/handlers/mopidy/image-cache/index.spec.js +++ b/backend/src/handlers/mopidy/image-cache/index.spec.js @@ -54,7 +54,7 @@ describe('ImageCache', () => { ImageCache.check('mopidy::library.getImages', [['uri123']]) .then((result) => { expect(result).toEqual({ image: 'cached123' }) - expect(logger.info).toHaveBeenCalledWith('Using cache', { key: 'mopidy::library.getImages#uri123' }) + expect(logger.info).toHaveBeenCalledWith('FOUND CACHED IMAGE', { key: 'uri123' }) done() }) }) diff --git a/backend/src/handlers/mopidy/index.js b/backend/src/handlers/mopidy/index.js index c42e0655..f29dc0a4 100644 --- a/backend/src/handlers/mopidy/index.js +++ b/backend/src/handlers/mopidy/index.js @@ -20,10 +20,6 @@ const isValidTrack = (key, data) => { return Spotify.validateTrack(data.uri) } -const sendToClient = (bcast, ws, payload, data) => { - bcast.to(ws, payload, data) -} - const logEvent = (headers, params, response, context) => { EventLogger({ encoded_key: headers.encoded_key }, params, response, context) } @@ -39,7 +35,7 @@ const MopidyHandler = (payload, ws, bcast, mopidy) => { payload.encoded_key, data ).then((obj) => { if (obj.image) { - sendToClient(bcast, ws, payload, obj.image) + bcast.to(ws, payload, obj.image, MessageType.IMAGE) } else { const apiCall = StrToFunction(mopidy, key) logEvent(payload, data, null, MessageType.OUTGOING_MOPIDY) @@ -51,7 +47,7 @@ const MopidyHandler = (payload, ws, bcast, mopidy) => { if (obj.addToCache) obj.addToCache(response) } - sendToClient(bcast, ws, payload, response) + bcast.to(ws, payload, response) } (data ? apiCall(data) : apiCall()) @@ -61,7 +57,7 @@ const MopidyHandler = (payload, ws, bcast, mopidy) => { }) }).catch((err) => { payload.encoded_key = Mopidy.VALIDATION_ERROR - sendToClient(bcast, ws, payload, err.message) + bcast.to(ws, payload, err.message) }) } diff --git a/backend/src/handlers/mopidy/index.spec.js b/backend/src/handlers/mopidy/index.spec.js index c1a8b743..748a7bbe 100644 --- a/backend/src/handlers/mopidy/index.spec.js +++ b/backend/src/handlers/mopidy/index.spec.js @@ -202,7 +202,7 @@ describe('MopidyHandler', () => { const mopidy = 'mopidy' const payload = { encoded_key: 'mopidy::library.getImages', data: [['12345zsdf23456']] } const trackMock = jest.fn().mockResolvedValue() - const cacheMock = jest.fn().mockResolvedValue({ image: 'image' }) + const cacheMock = jest.fn().mockResolvedValue({ image: 'cachedImageData' }) jest.spyOn(Spotify, 'validateTrack').mockImplementation(trackMock) jest.spyOn(ImageCache, 'check').mockImplementation(cacheMock) @@ -215,6 +215,7 @@ describe('MopidyHandler', () => { expect(broadcastMock).toHaveBeenCalledWith( ws, { data: [['12345zsdf23456']], encoded_key: 'mopidy::library.getImages' }, + 'cachedImageData', 'image' ) done() diff --git a/backend/src/utils/broadcaster/index.js b/backend/src/utils/broadcaster/index.js index 0c1217d5..f39b4877 100644 --- a/backend/src/utils/broadcaster/index.js +++ b/backend/src/utils/broadcaster/index.js @@ -19,7 +19,7 @@ const Broadcaster = { }) }, - toAllGeneric: (socket, key, message) => { + toAll: (socket, key, message) => { const payload = Payload.encodeToJson(key, message) try { @@ -37,7 +37,7 @@ const Broadcaster = { EventLogger({ encoded_key: 'state' }, null, payload, MessageType.OUTGOING_API_ALL) socket.emit(MessageType.MOPIDY, payload) } catch (e) { - logger.error('Broadcaster#toAll', { message: e.message }) + logger.error('Broadcaster#toAllMopidy', { message: e.message }) } } } diff --git a/backend/src/utils/broadcaster/index.spec.js b/backend/src/utils/broadcaster/index.spec.js index 14ed505b..12728171 100644 --- a/backend/src/utils/broadcaster/index.spec.js +++ b/backend/src/utils/broadcaster/index.spec.js @@ -89,7 +89,7 @@ describe('Broadcaster', () => { }) }) - describe('#toAllGeneric', () => { + describe('#toAll', () => { it('handles call', () => { const sendMock = jest.fn() const socketMock = { @@ -98,7 +98,7 @@ describe('Broadcaster', () => { const key = 'mopidy::playback.next' const message = 'hello mum' - broadcaster.toAllGeneric(socketMock, key, message) + broadcaster.toAll(socketMock, key, message) expect(sendMock.mock.calls.length).toEqual(1) expect(sendMock.mock.calls[0][0]).toEqual('message') expect(sendMock.mock.calls[0][1]).toEqual('{"key":"mopidy::playback.next","data":"hello mum"}') @@ -116,7 +116,7 @@ describe('Broadcaster', () => { const key = 'mopidy::playback.next' const message = 'hello mum' - broadcaster.toAllGeneric(socketMock, key, message) + broadcaster.toAll(socketMock, key, message) expect(sendMock.mock.calls[0]).toEqual(['message', '{"key":"mopidy::playback.next","data":"hello mum"}']) expect(logger.error.mock.calls[0][0]).toEqual('Broadcaster#toAll') expect(logger.error.mock.calls[0][1]).toEqual({ message: 'oops' }) @@ -149,7 +149,7 @@ describe('Broadcaster', () => { broadcaster.toAllMopidy(socketMock, message) expect(sendMock.mock.calls[0]).toEqual(['mopidy', '{"online":false}']) - expect(logger.error.mock.calls[0][0]).toEqual('Broadcaster#toAll') + expect(logger.error.mock.calls[0][0]).toEqual('Broadcaster#toAllMopidy') expect(logger.error.mock.calls[0][1]).toEqual({ message: 'oops' }) }) }) diff --git a/backend/src/utils/track/index.js b/backend/src/utils/track/index.js index 0a019355..b4e48bdc 100644 --- a/backend/src/utils/track/index.js +++ b/backend/src/utils/track/index.js @@ -1,10 +1,25 @@ import Track from 'services/mongodb/models/track' +import Image from 'services/mongodb/models/image' import logger from 'config/winston' export function findTracks (uris) { return new Promise((resolve, reject) => { - Track.find({'_id': { $in: uris }}) - .then(tracks => resolve(tracks)) + Track.find({ _id: { $in: uris } }) + .then(tracks => { + logger.info('FOUND CACHED TRACKS', { keys: uris }) + return resolve(tracks) + }) + .catch(err => reject(err)) + }) +} + +export function findImages (uris) { + return new Promise((resolve, reject) => { + Image.find({ uri: { $in: uris } }) + .then(images => { + logger.info('FOUND CACHED IMAGES', { keys: uris }) + return resolve(images) + }) .catch(err => reject(err)) }) } @@ -30,4 +45,4 @@ export function addTracks (uris, user) { }) } -export default { findTracks, addTracks } +export default { findTracks, findImages, addTracks } diff --git a/backend/src/utils/track/index.spec.js b/backend/src/utils/track/index.spec.js index abe1a5b5..11d2d033 100644 --- a/backend/src/utils/track/index.spec.js +++ b/backend/src/utils/track/index.spec.js @@ -1,8 +1,10 @@ -import { findTracks, addTracks } from './index' +import { findTracks, addTracks, findImages } from './index' import Track from 'services/mongodb/models/track' +import Image from 'services/mongodb/models/image' import logger from 'config/winston' jest.mock('config/winston') jest.mock('services/mongodb/models/track') +jest.mock('services/mongodb/models/image') const userObject = { _id: '999', @@ -10,11 +12,11 @@ const userObject = { } describe('trackUtils', () => { - describe('#findTracks', () => { - afterEach(() => { - jest.clearAllMocks() - }) + afterEach(() => { + jest.clearAllMocks() + }) + describe('#findTracks', () => { it('makes a call to findOne Track document', () => { expect.assertions(1) Track.find.mockResolvedValue([{ _id: '123' }]) @@ -36,6 +38,28 @@ describe('trackUtils', () => { }) }) + describe('#findImages', () => { + it('makes a call to findImages', () => { + expect.assertions(1) + Image.find.mockResolvedValue([{ _id: '123' }]) + return findImages('123').then(() => { + expect(Image.find).toHaveBeenCalledWith({ + uri: { + $in: '123' + } + }) + }) + }) + + it('handles errors', () => { + expect.assertions(1) + Image.find.mockRejectedValue(new Error('bang')) + return findImages('uri123').catch((error) => { + expect(error.message).toEqual('bang') + }) + }) + }) + describe('#addTrack', () => { const trackObject = { trackUri: '123' } const fakeDate = new Date(1222222224332) diff --git a/backend/src/utils/transformer/index.js b/backend/src/utils/transformer/index.js index 8b94726b..992fab87 100644 --- a/backend/src/utils/transformer/index.js +++ b/backend/src/utils/transformer/index.js @@ -84,9 +84,12 @@ const Transform = { clearSetTimeout(recommendTimer) if (data && data.length > 0) return resolve(TransformTrack(data[0].track)) return resolve() + case Mopidy.LIBRARY_GET_IMAGES: + const uri = Object.keys(data)[0] + const image = data[uri][0].uri + return resolve({ [uri]: image }) case Mopidy.TRACKLIST_CLEAR: case Mopidy.CONNECTION_ERROR: - case Mopidy.LIBRARY_GET_IMAGES: case Mopidy.MIXER_GET_VOLUME: case Mopidy.MIXER_SET_VOLUME: case Mopidy.PLAYBACK_GET_TIME_POSITION: diff --git a/backend/src/utils/transformer/index.spec.js b/backend/src/utils/transformer/index.spec.js index d2d75b8e..421fa351 100644 --- a/backend/src/utils/transformer/index.spec.js +++ b/backend/src/utils/transformer/index.spec.js @@ -152,9 +152,8 @@ describe('Transformer', () => { }) describe('event:volumeChanged', () => { - data = { volume: 99 } - it('returns the volume data passed in', () => { + data = { volume: 99 } expect.assertions(1) return Transformer.mopidyCoreMessage(h('mopidy::event:volumeChanged'), data) .then(returnData => expect(returnData).toEqual(data.volume)) @@ -162,9 +161,8 @@ describe('Transformer', () => { }) describe('event:playbackStateChanged', () => { - data = { new_state: 'playing' } - it('returns the playback state data passed in', () => { + data = { new_state: 'playing' } expect.assertions(1) return Transformer.mopidyCoreMessage(h('mopidy::event:playbackStateChanged'), data) .then(returnData => expect(returnData).toEqual(data.new_state)) @@ -173,9 +171,15 @@ describe('Transformer', () => { describe('library.getImages', () => { it('returns the data passed in', () => { + data = { + 'spotify123abc': [ + { uri: 'path/to/img/1' }, + { uri: 'path/to/img/2' } + ] + } expect.assertions(1) return Transformer.message(h('mopidy::library.getImages'), data) - .then(returnData => expect(returnData).toEqual(data)) + .then(returnData => expect(returnData).toEqual({'spotify123abc': 'path/to/img/1'})) }) }) diff --git a/backend/src/utils/transformer/transformers/mopidy/track/index.js b/backend/src/utils/transformer/transformers/mopidy/track/index.js index 363d3f0a..ad772cb8 100644 --- a/backend/src/utils/transformer/transformers/mopidy/track/index.js +++ b/backend/src/utils/transformer/transformers/mopidy/track/index.js @@ -7,6 +7,10 @@ export default function (json) { length: json.length || json.duration_ms } + if (json.image) { + payload.image = json.image + } + if (json.album) { payload.album = { uri: json.album.uri, @@ -14,7 +18,7 @@ export default function (json) { year: json.album.date } - if (json.album.images && json.album.images.length > 0) { + if (!payload.image && json.album.images && json.album.images.length > 0) { payload.image = json.album.images[0].url } } diff --git a/backend/src/utils/transformer/transformers/mopidy/track/index.spec.js b/backend/src/utils/transformer/transformers/mopidy/track/index.spec.js index a0292644..623195b3 100644 --- a/backend/src/utils/transformer/transformers/mopidy/track/index.spec.js +++ b/backend/src/utils/transformer/transformers/mopidy/track/index.spec.js @@ -8,11 +8,13 @@ describe('TransformerTrack', () => { it('transforms the track', () => { const albumTrack = payload[0] albumTrack.addedBy = 'duncan' + albumTrack.image = 'http://path/to/image' expect(TransformerTrack(albumTrack)).toEqual({ track: { uri: 'spotify:track:1yzSSn5Sj1azuo7RgwvDb3', name: 'No Time for Caution', + image: 'http://path/to/image', year: '2014', length: 246000, addedBy: 'duncan', diff --git a/backend/src/utils/transformer/transformers/mopidy/tracklist/index.js b/backend/src/utils/transformer/transformers/mopidy/tracklist/index.js index 08da040d..e1c27de8 100644 --- a/backend/src/utils/transformer/transformers/mopidy/tracklist/index.js +++ b/backend/src/utils/transformer/transformers/mopidy/tracklist/index.js @@ -1,18 +1,30 @@ import TransformTrack from 'utils/transformer/transformers/mopidy/track' -import { findTracks } from 'utils/track' +import { findTracks, findImages } from 'utils/track' const Tracklist = (json) => { return new Promise((resolve) => { const trackUris = json.map(data => data.uri) + const imageUris = json.filter(data => data.album).map(data => data.album.uri) + const requests = [ + findTracks(trackUris), + findImages(imageUris) + ] - findTracks(trackUris).then(tracks => { + Promise.all(requests).then((responses) => { + const tracks = responses[0] + const images = responses[1] const decoratedTracks = json.map(data => { const trackData = tracks.find(track => track._id === data.uri) + const imageData = images.find(image => image.uri === (data.album && data.album.uri)) if (trackData) { data.addedBy = trackData.addedBy.reverse().map(user => user[0]) } + if (imageData) { + data.image = imageData.data[data.album.uri][0].uri + } + return TransformTrack(data) }) diff --git a/backend/src/utils/transformer/transformers/mopidy/tracklist/index.spec.js b/backend/src/utils/transformer/transformers/mopidy/tracklist/index.spec.js index 90fb3774..091b8b44 100644 --- a/backend/src/utils/transformer/transformers/mopidy/tracklist/index.spec.js +++ b/backend/src/utils/transformer/transformers/mopidy/tracklist/index.spec.js @@ -1,6 +1,7 @@ import TransformerTracklist from './index' import fs from 'fs' import lolex from 'lolex' +jest.mock('config/winston') const firstTrack = { _id: 'spotify:track:1yzSSn5Sj1azuo7RgwvDb3', @@ -22,10 +23,22 @@ const secondTrack = { ]], __v: 0 } +const firstImage = { + _id: '123', + uri: 'spotify:album:5OVGwMCexoHavOar6v4al5', + data: { + 'spotify:album:5OVGwMCexoHavOar6v4al5': [ + { uri: 'path/to/image/1' }, + { uri: 'path/to/image/2' } + ] + } +} const mockTrackData = [firstTrack, secondTrack] +const mockImageData = [firstImage] jest.mock('utils/track', () => ({ - findTracks: jest.fn().mockImplementation(() => Promise.resolve(mockTrackData)) + findTracks: jest.fn().mockImplementation(() => Promise.resolve(mockTrackData)), + findImages: jest.fn().mockImplementation(() => Promise.resolve(mockImageData)) })) describe('TransformerTracklist', () => { @@ -44,8 +57,16 @@ describe('TransformerTracklist', () => { it('transforms it', () => { expect.assertions(1) return TransformerTracklist(payload).then(transformedPayload => { - expect(transformedPayload).toEqual([{ 'track': { 'addedBy': [{ '_id': '123', 'fullname': 'Big Rainbowhead' }], 'album': { 'name': 'Interstellar: Original Motion Picture Soundtrack (Deluxe Digital Version)', 'uri': 'spotify:album:5OVGwMCexoHavOar6v4al5', 'year': '2014' }, 'artist': { 'name': 'Hans Zimmer', 'uri': 'spotify:artist:0YC192cP3KPCRWx8zr8MfZ' }, 'length': 246000, 'name': 'No Time for Caution', 'uri': 'spotify:track:1yzSSn5Sj1azuo7RgwvDb3', 'year': '2014' } }, { 'track': { 'artist': { 'name': 'Joan Baez', 'uri': 'local:artist:md5:23327ccea5c999183cc88701751f8c73' }, 'composer': { 'name': 'Peter Schickele', 'uri': 'local:artist:md5:af20b04e7ff55f56afec2be1f36afe94' }, 'genre': 'Soundtrack', 'length': 123973, 'name': 'Silent Running', 'uri': 'local:track:Soundtracks/Silent%20Running%20OST/Silent%20Running%20' } }]) + expect(transformedPayload).toEqual([{ 'track': { 'addedBy': [{ '_id': '123', 'fullname': 'Big Rainbowhead' }], 'album': { 'name': 'Interstellar: Original Motion Picture Soundtrack (Deluxe Digital Version)', 'uri': 'spotify:album:5OVGwMCexoHavOar6v4al5', 'year': '2014' }, 'artist': { 'name': 'Hans Zimmer', 'uri': 'spotify:artist:0YC192cP3KPCRWx8zr8MfZ' }, 'image': 'path/to/image/1', 'length': 246000, 'name': 'No Time for Caution', 'uri': 'spotify:track:1yzSSn5Sj1azuo7RgwvDb3', 'year': '2014' } }, { 'track': { 'artist': { 'name': 'Joan Baez', 'uri': 'local:artist:md5:23327ccea5c999183cc88701751f8c73' }, 'composer': { 'name': 'Peter Schickele', 'uri': 'local:artist:md5:af20b04e7ff55f56afec2be1f36afe94' }, 'genre': 'Soundtrack', 'length': 123973, 'name': 'Silent Running', 'uri': 'local:track:Soundtracks/Silent%20Running%20OST/Silent%20Running%20' } }]) }) }) + + it('catches errors', () => { + expect.assertions(1) + TransformerTracklist('something broke') + .catch(err => { + expect(err.message).toEqual('json.map is not a function') + }) + }) }) }) diff --git a/frontend/package.json b/frontend/package.json index 38e53cf0..bb6dce59 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -55,7 +55,15 @@ ], "snapshotSerializers": [ "enzyme-to-json/serializer" - ] + ], + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 100, + "statements": 100 + } + } }, "standard": { "parser": "babel-eslint", diff --git a/frontend/src/__mockData__/api/index.js b/frontend/src/__mockData__/api/index.js index 6ddee72b..fb5d2836 100644 --- a/frontend/src/__mockData__/api/index.js +++ b/frontend/src/__mockData__/api/index.js @@ -1,5 +1,5 @@ const api = () => { - return JSON.parse('[{"track":{"uri":"spotify:track:1yzSSn5Sj1azuo7RgwvDb3","name":"No Time for Caution","length":246000,"start_time":1517488074076,"album":{"uri":"spotify:album:5OVGwMCexoHavOar6v4al5","name":"Interstellar: Original Motion Picture Soundtrack (Deluxe Digital Version)","year":"2014"},"artist":{"uri":"spotify:artist:0YC192cP3KPCRWx8zr8MfZ","name":"Hans Zimmer"},"year":"2014", "addedBy":[{"_id": "123", "fullname": "Big Rainbowhead", "addedAt": "2019-12-17T13:11:37.316Z"}]}},{"track":{"uri":"local:track:Soundtracks/Silent%20Running%20OST/Silent%20Running%20","name":"Silent Running","length":123973,"start_time":1517488674076,"composer":{"uri":"local:artist:md5:af20b04e7ff55f56afec2be1f36afe94","name":"Peter Schickele"},"genre":"Soundtrack","artist":{"uri":"local:artist:md5:23327ccea5c999183cc88701751f8c73","name":"Joan Baez"}}}]') + return JSON.parse('[{"track":{"uri":"spotify:track:1yzSSn5Sj1azuo7RgwvDb3","name":"No Time for Caution","length":246000,"start_time":1517488074076,"image":"http://path/to/cool/image.png","album":{"uri":"spotify:album:5OVGwMCexoHavOar6v4al5","name":"Interstellar: Original Motion Picture Soundtrack (Deluxe Digital Version)","year":"2014"},"artist":{"uri":"spotify:artist:0YC192cP3KPCRWx8zr8MfZ","name":"Hans Zimmer"},"year":"2014","addedBy":[{"_id":"123","fullname":"Big Rainbowhead","addedAt":"2019-12-17T13:11:37.316Z"}]}},{"track":{"uri":"spotify:track:1yzSSn5Sj1azuo7Rgwvdunc","name":"Is this sucker going live anytime soon","length":246000,"start_time":1517488074076,"album":{"uri":"spotify:album:5OVGwMCexoHavOar6vdunc","name":"Expectation: Things that may never happen volume 1","year":"2014"},"artist":{"uri":"spotify:artist:0YC192cP3KPCRWx8zrdunc","name":"The chancer"},"year":"2018","addedBy":[{"_id":"666","fullname":"Duncan Robertson","addedAt":"2019-12-17T13:11:37.316Z"}]}},{"track":{"uri":"soundcloud:track:5OVGwMCexoHacompose","name":"Silent Running","length":123973,"start_time":1517488674076,"composer":{"uri":"soundcloud:album:5OVGwMCexoHacompose","name":"Peter Schickele"},"genre":"Soundtrack","artist":{"uri":"soundcloud:artist:5OVGwMCexoHacompose","name":"Joan Baez"}}}]') } export default api diff --git a/frontend/src/actions/index.js b/frontend/src/actions/index.js index 0246dd22..7c69024e 100644 --- a/frontend/src/actions/index.js +++ b/frontend/src/actions/index.js @@ -122,24 +122,17 @@ export const getState = () => { export const getImage = (uri) => { return { - type: Types.SEND, + type: Types.IMAGE_REQUEST, key: MopidyApi.LIBRARY_GET_IMAGES, params: [[uri]], uri: uri } } -export const newImage = (uri) => { - return { - type: Types.NEW_IMAGE, - uri - } -} - -export const resolveImage = (data) => { +export const resolveImage = (image) => { return { type: Types.RESOLVE_IMAGE, - data + image } } diff --git a/frontend/src/actions/index.spec.js b/frontend/src/actions/index.spec.js index 66803f5d..f1252fbb 100644 --- a/frontend/src/actions/index.spec.js +++ b/frontend/src/actions/index.spec.js @@ -131,7 +131,7 @@ describe('actions', () => { it('should handle getImage', () => { const uri = 'spotify:track:0c41pMosF5Kqwwegcps8ES' const expectedAction = { - type: Types.SEND, + type: Types.IMAGE_REQUEST, key: MopidyApi.LIBRARY_GET_IMAGES, params: [[uri]], uri @@ -139,22 +139,13 @@ describe('actions', () => { expect(actions.getImage(uri)).toEqual(expectedAction) }) - it('should handle newImage', () => { - const uri = 'spotify:track:0c41pMosF5Kqwwegcps8ES' - const expectedAction = { - type: Types.NEW_IMAGE, - uri - } - expect(actions.newImage(uri)).toEqual(expectedAction) - }) - it('should handle resolveImage', () => { - const data = 'spotify:track:0c41pMosF5Kqwwegcps8ES' + const image = { 'spotify:track:0c41pMosF5Kqwwegcps8ES': 'path/to/image' } const expectedAction = { type: Types.RESOLVE_IMAGE, - data + image } - expect(actions.resolveImage(data)).toEqual(expectedAction) + expect(actions.resolveImage(image)).toEqual(expectedAction) }) it('should handle getTrackList', () => { diff --git a/frontend/src/components/current-track/index.spec.js b/frontend/src/components/current-track/index.spec.js index d60852fb..cb90cffc 100644 --- a/frontend/src/components/current-track/index.spec.js +++ b/frontend/src/components/current-track/index.spec.js @@ -44,7 +44,7 @@ describe('CurrentTrack', () => { describe('composer', () => { it('renders track', () => { - track = MockTrackListJson()[1].track + track = MockTrackListJson()[2].track wrapper = shallow( + + + + Is this sucker going live anytime soon + + + The chancer + + + ( + 4:06 + ) + + + + + + + + + + Is this sucker going live anytime soon + + + The chancer + + + ( + 4:06 + ) + + + + + + @@ -213,7 +298,51 @@ exports[`Tracklist tracklist but nothing cued up does not mark anything as curre + + + + Is this sucker going live anytime soon + + + The chancer + + + ( + 4:06 + ) + + + + + + - Added: - 17 Dec 2019 @ 1:11 pm - + + + + + + + 17 Dec 2019 @ 1:11 pm + + - + Big Rainbowhead + + + + } disabled={false} eventsEnabled={true} @@ -54,6 +70,22 @@ exports[`AddedBy when addedBy information provided and there is play history dis + + + + + + 17 Dec 2019 @ 1:11 pm + + - + Big Rainbowhead2 + + + } disabled={false} diff --git a/frontend/src/components/tracklist/added-by/index.js b/frontend/src/components/tracklist/added-by/index.js index cec075b5..ae6f6962 100644 --- a/frontend/src/components/tracklist/added-by/index.js +++ b/frontend/src/components/tracklist/added-by/index.js @@ -3,13 +3,7 @@ import PropTypes from 'prop-types' import { List, Popup, Icon, Image } from 'semantic-ui-react' import dateFormat from 'dateformat' -const firstTime = (user) => { - if (!user) return 'First time played.' - - return Added: {dateFormat(user.addedAt, 'dd mmm yyyy @ h:MM tt')} -} - -const addedByContent = (user, users) => { +const addedByContent = (users) => { if (users.length) { return ( @@ -31,7 +25,7 @@ const addedByContent = (user, users) => { ) } - return firstTime(user) + return 'First time played.' } const userPicture = user => { @@ -40,14 +34,11 @@ const userPicture = user => { } const AddedBy = ({ users = [] }) => { - const currentUser = users[0] - const previousUsers = users.slice(0, -1) - return ( ) } diff --git a/frontend/src/components/tracklist/index.js b/frontend/src/components/tracklist/index.js index a5166743..c9ec3d7e 100644 --- a/frontend/src/components/tracklist/index.js +++ b/frontend/src/components/tracklist/index.js @@ -39,8 +39,9 @@ const removeTrack = (uri, cb) => { const imageChooser = (disabled, track, images, isCurrent, onRemoveTrack, hasBeenPlayed) => { let image - if (images && track.album) image = images[track.album.uri] - if (images && track.composer) image = images[track.composer.uri] + if (track.image) image = track.image + if (!image && images && track.album) image = images[track.album.uri] + if (!image && images && track.composer) image = images[track.composer.uri] if (!image) image = defaultImage return trackImage({ diff --git a/frontend/src/components/tracklist/index.spec.js b/frontend/src/components/tracklist/index.spec.js index 419fd888..23bf19bf 100644 --- a/frontend/src/components/tracklist/index.spec.js +++ b/frontend/src/components/tracklist/index.spec.js @@ -61,7 +61,7 @@ describe('Tracklist', () => { wrapper.find('.item').at(1).find('img').simulate('click') expect(onRemoveMock.mock.calls.length).toEqual(1) - expect(onRemoveMock.mock.calls[0][0]).toEqual('local:track:Soundtracks/Silent%20Running%20OST/Silent%20Running%20') + expect(onRemoveMock.mock.calls[0][0]).toEqual('spotify:track:1yzSSn5Sj1azuo7Rgwvdunc') }) }) diff --git a/frontend/src/constants/common.js b/frontend/src/constants/common.js index 1ac8c08a..8e642c65 100644 --- a/frontend/src/constants/common.js +++ b/frontend/src/constants/common.js @@ -7,9 +7,9 @@ export default { DISCONNECT: 'actionDisconnect', DISCONNECTED: 'actionDisconnected', DROP_TYPES: ['__NATIVE_URL__'], - NEW_IMAGE: 'actionNewImage', RESOLVE_IMAGE: 'actionResolveImage', SEND: 'actionSend', + IMAGE_REQUEST: 'actionRequestImage', STORE_TOKEN: 'actionStoreToken', CLEAR_STORE_TOKEN: 'actionClearStoreToken', UPDATE_VOLUME: 'actionUpdateVolume', diff --git a/frontend/src/containers/jukebox-middleware/index.js b/frontend/src/containers/jukebox-middleware/index.js index 3701884d..d8fcbb45 100644 --- a/frontend/src/containers/jukebox-middleware/index.js +++ b/frontend/src/containers/jukebox-middleware/index.js @@ -1,9 +1,7 @@ import io from 'socket.io-client' import * as actions from 'actions' -import MopidyApi from 'constants/mopidy-api' import Constants from 'constants/common' import SearchConst from 'search/constants' -import { findImageInCache } from 'utils/images' import { trackProgressTimer } from 'utils/time' import onMessageHandler from 'utils/on-message-handler' import Payload from 'utils/payload' @@ -15,9 +13,7 @@ const JukeboxMiddleware = (() => { let progressTimer = null return store => next => action => { - const isImageRequest = () => action.key === MopidyApi.LIBRARY_GET_IMAGES - const imageIsCached = () => findImageInCache(action.uri, store.getState().assets) - const preStoreImage = () => store.dispatch(actions.newImage(action.uri)) + const imageIsCached = () => store.getState().assets[action.uri] const getJWT = () => store.getState().settings.token const packMessage = () => Payload.encodeToJson(getJWT(store), action.key, action.params) @@ -34,10 +30,12 @@ const JukeboxMiddleware = (() => { } const onClose = _evt => store.dispatch(actions.wsDisconnect()) const onMessage = data => onMessageHandler(store, data, progressTimer) + const onImage = data => onMessageHandler(store, data, progressTimer) const onSearchResults = data => onMessageHandler(store, data, progressTimer) const onConnect = () => { if (socket != null) socket.close() socket = io(url, { transports: ['websocket'] }) + socket.on('image', onImage) socket.on('search', onSearchResults) socket.on('mopidy', mopidyStateChange) socket.on('message', onMessage) @@ -57,12 +55,12 @@ const JukeboxMiddleware = (() => { case Constants.DISCONNECT: return onDisconnect() case Constants.SEND: - if (isImageRequest() && imageIsCached()) return - if (isImageRequest()) preStoreImage() - return socket.emit('message', packMessage()) case SearchConst.SEARCH: return socket.emit('search', packMessage()) + case Constants.IMAGE_REQUEST: + if (imageIsCached()) return + return socket.emit('message', packMessage()) default: return next(action) } diff --git a/frontend/src/containers/jukebox-middleware/index.spec.js b/frontend/src/containers/jukebox-middleware/index.spec.js index d5f928a9..5f8fbd3d 100644 --- a/frontend/src/containers/jukebox-middleware/index.spec.js +++ b/frontend/src/containers/jukebox-middleware/index.spec.js @@ -130,29 +130,31 @@ describe('JukeboxMiddleware', () => { ]) // fetch image not in the cache - store.clearActions() + mockEmit.mockClear() JukeboxMiddleware(store)(next)({ - type: Constants.SEND, + type: Constants.IMAGE_REQUEST, key: MopidyApi.LIBRARY_GET_IMAGES, params: 'params', uri: '12345678' }) - actions = store.getActions() - expect(actions).toEqual([{ type: 'actionNewImage', uri: '12345678' }]) + expect(mockEmit.mock.calls).toEqual([['message', '{"jwt":"token","key":"mopidy::library.getImages","data":"params"}']]) - // fetch image already in the cache + // don't fetch image already in the cache store.clearActions() + mockEmit.mockClear() const imageInStore = mockStore({ - assets: [{ ref: 'imageincache', uri: 'image123' }] + assets: { 'imageincache': 'image123' }, + settings: { token: 'token' } }) JukeboxMiddleware(imageInStore)(next)({ - type: Constants.SEND, + type: Constants.IMAGE_REQUEST, key: MopidyApi.LIBRARY_GET_IMAGES, params: 'params', uri: 'imageincache' }) actions = store.getActions() expect(actions).toEqual([]) + expect(mockEmit.mock.calls).toEqual([]) // send message with params mockEmit.mockClear() diff --git a/frontend/src/reducers/assets/index.js b/frontend/src/reducers/assets/index.js index 0d323dae..0620c6ce 100644 --- a/frontend/src/reducers/assets/index.js +++ b/frontend/src/reducers/assets/index.js @@ -1,20 +1,11 @@ import Types from 'constants/common' -const initalState = [] -const MAX_IMAGES_IN_CACHE = 200 +const initalState = {} const assets = (state = initalState, action) => { switch (action.type) { - case Types.NEW_IMAGE: - return (state.find(a => action.uri && a.ref === action.uri)) - ? state - : [ ...state, { ref: action.uri } ] case Types.RESOLVE_IMAGE: - return state.map(asset => - (action.data[asset.ref] && action.data[asset.ref][0]) - ? { ...asset, uri: action.data[asset.ref][0].uri } - : asset - ).slice(0, MAX_IMAGES_IN_CACHE) + return { ...state, ...action.image } default: return state } diff --git a/frontend/src/reducers/assets/index.spec.js b/frontend/src/reducers/assets/index.spec.js index 45fd231a..cd93299d 100644 --- a/frontend/src/reducers/assets/index.spec.js +++ b/frontend/src/reducers/assets/index.spec.js @@ -3,41 +3,27 @@ import Types from 'constants/common' describe('assets', () => { it('handles default state', () => { - expect(reducer(undefined, {})).toEqual([]) - }) - - it('handles a new image', () => { - expect(reducer(undefined, { - type: Types.NEW_IMAGE, - uri: '123456789asdfghj' - })).toEqual([{ 'ref': '123456789asdfghj' }]) - }) - - it('handles a the same image and does not add again', () => { - expect(reducer([{ 'ref': '123456789asdfghj' }], { - type: Types.NEW_IMAGE, - uri: '123456789asdfghj' - })).toEqual([{ 'ref': '123456789asdfghj' }]) + expect(reducer(undefined, {})).toEqual({}) }) it('handles a resolving an image', () => { - const assets = [ - { 'ref': '123456789asdfghj' }, - { 'ref': 'xdskjhdskjdhskjd' } - ] - const imageData = { - '123456789asdfghj': [ - { uri: 'large image' }, - { uri: 'medium image' } - ] + const assets = { + 'spotify1': 'path/to/1', + 'spotify2': 'path/to/2', + 'spotify4': 'path/to/4' + } + const image = { + 'spotify3': 'path/to/3' } expect(reducer(assets, { type: Types.RESOLVE_IMAGE, - data: imageData - })).toEqual([ - { 'ref': '123456789asdfghj', 'uri': 'large image' }, - { 'ref': 'xdskjhdskjdhskjd' } - ]) + image + })).toEqual({ + spotify1: 'path/to/1', + spotify2: 'path/to/2', + spotify3: 'path/to/3', + spotify4: 'path/to/4' + }) }) }) diff --git a/frontend/src/selectors/index.js b/frontend/src/selectors/index.js index d29ba9d4..5ba1f6bb 100644 --- a/frontend/src/selectors/index.js +++ b/frontend/src/selectors/index.js @@ -1,5 +1,4 @@ import { createSelector } from 'reselect' -import { findImageInCache } from 'utils/images' const getCurrentTrack = (state) => state.track const getTrackList = (state) => state.tracklist @@ -9,7 +8,7 @@ export const getCurrentTrackImageInCache = createSelector( [getCurrentTrack, getAssets], (track, cache) => { if (!track) { return null } - return findImageInCache(track.album.uri, cache) + return track.album.image || cache[track.album.uri] } ) @@ -18,7 +17,7 @@ export const getTracklistImagesInCache = createSelector( (tracklist, cache) => { const images = {} tracklist.forEach(track => { - images[track.album.uri] = findImageInCache(track.album.uri, cache) + images[track.album.uri] = track.album.image || cache[track.album.uri] }) return images } diff --git a/frontend/src/selectors/index.spec.js b/frontend/src/selectors/index.spec.js index a8df32ff..3cd6665c 100644 --- a/frontend/src/selectors/index.spec.js +++ b/frontend/src/selectors/index.spec.js @@ -1,11 +1,11 @@ import * as selectors from './index' describe('selectors', () => { - const cache = [ - { ref: 'spotify:track:0c41pMosF5Kqwwegcps8ES' }, - { ref: 'spotify:track:0c41pMosF5Kqwweg123455', uri: 'path/to/file' }, - { ref: 'spotify:track:0c41pMosF5Kqwwegxxxxxx', uri: 'path/to/file1' } - ] + const cache = { + 'spotify:track:0c41pMosF5Kqwwegcps8ES': null, + 'spotify:track:0c41pMosF5Kqwweg123455': 'path/to/file', + 'spotify:track:0c41pMosF5Kqwwegxxxxxx': 'path/to/file1' + } describe('getCurrentTrackImageInCache', () => { const track = { album: { uri: 'spotify:track:0c41pMosF5Kqwweg123455' } } diff --git a/frontend/src/utils/images/index.js b/frontend/src/utils/images/index.js deleted file mode 100644 index 315442bf..00000000 --- a/frontend/src/utils/images/index.js +++ /dev/null @@ -1,5 +0,0 @@ -export const findImageInCache = (uri, cache) => { - const index = cache.findIndex(asset => asset.ref === uri) - if (cache[index]) { return cache[index].uri } - return null -} diff --git a/frontend/src/utils/images/index.spec.js b/frontend/src/utils/images/index.spec.js deleted file mode 100644 index ec1ecc5c..00000000 --- a/frontend/src/utils/images/index.spec.js +++ /dev/null @@ -1,20 +0,0 @@ -import * as images from './index' - -describe('images', () => { - describe('findImageInCache', () => { - const cache = [ - { ref: 'spotify:track:0c41pMosF5Kqwwegcps8ES' }, - { ref: 'spotify:track:0c41pMosF5Kqwweg123455', uri: 'path/to/file' } - ] - - it('handles correctly finding an image', () => { - const uri = 'spotify:track:0c41pMosF5Kqwweg123455' - expect(images.findImageInCache(uri, cache)).toEqual('path/to/file') - }) - - it('handles correctly not finding an image', () => { - const uri = 'spotify:track:xxxxx' - expect(images.findImageInCache(uri, cache)).toBeNull() - }) - }) -}) diff --git a/frontend/src/utils/on-message-handler/index.js b/frontend/src/utils/on-message-handler/index.js index 1e4b1edd..ecc0a77b 100644 --- a/frontend/src/utils/on-message-handler/index.js +++ b/frontend/src/utils/on-message-handler/index.js @@ -34,7 +34,9 @@ const imageUriChooser = (track) => { const addCurrentTrack = (track, store, progress) => { store.dispatch(actions.addCurrentTrack(track)) - store.dispatch(actions.getImage(imageUriChooser(track))) + if (!track.image) { + store.dispatch(actions.getImage(imageUriChooser(track))) + } const progressTimer = progress.set(0, track.length) if (store.getState().jukebox.playbackState === MopidyApi.PLAYING) progressTimer.start() } @@ -42,7 +44,9 @@ const addCurrentTrack = (track, store, progress) => { const addTrackList = (tracklist, store) => { store.dispatch(actions.addTrackList(tracklist)) tracklist.forEach(item => { - store.dispatch(actions.getImage(imageUriChooser(item.track))) + if (!item.track.image) { + store.dispatch(actions.getImage(imageUriChooser(item.track))) + } }) } diff --git a/frontend/src/utils/on-message-handler/index.spec.js b/frontend/src/utils/on-message-handler/index.spec.js index 24a192ed..bee2698b 100644 --- a/frontend/src/utils/on-message-handler/index.spec.js +++ b/frontend/src/utils/on-message-handler/index.spec.js @@ -37,7 +37,7 @@ describe('onMessageHandler', () => { it('checks track playing', () => { const payload = { data: { - track: MockTrackListJson()[0].track + track: MockTrackListJson()[1].track }, key: MopidyApi.PLAYBACK_GET_CURRENT_TRACK } @@ -55,7 +55,7 @@ describe('onMessageHandler', () => { expect(actions[1]).toEqual({ key: 'mopidy::library.getImages', params: [[payload.data.track.album.uri]], - type: 'actionSend', + type: 'actionRequestImage', uri: payload.data.track.album.uri }) expect(progressStartMock.mock.calls.length).toEqual(1) @@ -80,12 +80,7 @@ describe('onMessageHandler', () => { track: payload.data.track, type: 'actionAddCurrentTrack' }) - expect(actions[1]).toEqual({ - key: 'mopidy::library.getImages', - params: [[payload.data.track.album.uri]], - type: 'actionSend', - uri: payload.data.track.album.uri - }) + expect(actions.length).toEqual(1) expect(progressStartMock.mock.calls.length).toEqual(0) progressStartMock.mockClear() }) @@ -102,14 +97,14 @@ describe('onMessageHandler', () => { }) onMessageHandler(store, JSON.stringify(payload), progress) const actions = store.getActions() - expect(actions).toEqual([]) + expect(actions.length).toEqual(0) expect(progressStartMock.mock.calls.length).toEqual(0) progressStartMock.mockClear() }) }) describe('EVENT_TRACK_PLAYBACK_STARTED', () => { - it('checks track playing', () => { + it('checks track playing with image provided in payload', () => { const payload = { data: { track: MockTrackListJson()[0].track @@ -127,10 +122,33 @@ describe('onMessageHandler', () => { track: payload.data.track, type: 'actionAddCurrentTrack' }) + expect(actions.length).toEqual(1) + expect(progressStartMock.mock.calls.length).toEqual(1) + progressStartMock.mockClear() + }) + + it('checks track playing with no image provided in payload', () => { + const payload = { + data: { + track: MockTrackListJson()[1].track + }, + key: MopidyApi.EVENT_TRACK_PLAYBACK_STARTED + } + const store = mockStore({ + jukebox: { + playbackState: 'playing' + } + }) + onMessageHandler(store, JSON.stringify(payload), progress) + const actions = store.getActions() + expect(actions[0]).toEqual({ + track: payload.data.track, + type: 'actionAddCurrentTrack' + }) expect(actions[1]).toEqual({ key: 'mopidy::library.getImages', params: [[payload.data.track.album.uri]], - type: 'actionSend', + type: 'actionRequestImage', uri: payload.data.track.album.uri }) expect(progressStartMock.mock.calls.length).toEqual(1) @@ -195,7 +213,7 @@ describe('onMessageHandler', () => { }) onMessageHandler(store, JSON.stringify(payload), progress) const actions = store.getActions() - expect(actions).toEqual([]) + expect(actions.length).toEqual(0) expect(progressStartMock.mock.calls.length).toEqual(0) expect(progressStopMock.mock.calls.length).toEqual(0) }) @@ -216,16 +234,17 @@ describe('onMessageHandler', () => { }) expect(actions[1]).toEqual({ key: 'mopidy::library.getImages', - params: [['spotify:album:5OVGwMCexoHavOar6v4al5']], - type: 'actionSend', - uri: 'spotify:album:5OVGwMCexoHavOar6v4al5' + params: [['spotify:album:5OVGwMCexoHavOar6vdunc']], + type: 'actionRequestImage', + uri: 'spotify:album:5OVGwMCexoHavOar6vdunc' }) expect(actions[2]).toEqual({ key: 'mopidy::library.getImages', - params: [['local:artist:md5:af20b04e7ff55f56afec2be1f36afe94']], - type: 'actionSend', - uri: 'local:artist:md5:af20b04e7ff55f56afec2be1f36afe94' + params: [['soundcloud:album:5OVGwMCexoHacompose']], + type: 'actionRequestImage', + uri: 'soundcloud:album:5OVGwMCexoHacompose' }) + expect(actions.length).toEqual(3) }) }) @@ -233,9 +252,7 @@ describe('onMessageHandler', () => { it('handles resolving', () => { const payload = { key: MopidyApi.LIBRARY_GET_IMAGES, - data: { - track: MockTrackListJson()[0].track - } + image: { 'spotify1': 'path/to/url/1' } } const store = mockStore({}) onMessageHandler(store, JSON.stringify(payload), progress)