Browse Source

Add jellyfin playlist browse support

master
jeffvli 3 years ago
committed by Jeff
parent
commit
038f536be9
  1. 4
      src/api/api.ts
  2. 4
      src/api/controller.ts
  3. 115
      src/api/jellyfinApi.ts
  4. 8
      src/api/types.ts
  5. 5
      src/components/playlist/PlaylistList.tsx
  6. 9
      src/components/playlist/PlaylistView.tsx
  7. 12
      src/shared/utils.ts

4
src/api/api.ts

@ -279,8 +279,8 @@ const normalizeFolder = (item: any) => {
};
};
export const getPlaylist = async (id: string) => {
const { data } = await api.get(`/getPlaylist?id=${id}`);
export const getPlaylist = async (options: { id: string }) => {
const { data } = await api.get(`/getPlaylist?id=${options.id}`);
return normalizePlaylist(data.playlist);
};

4
src/api/controller.ts

@ -30,12 +30,12 @@ import {
updatePlaylistSongs,
updatePlaylistSongsLg,
} from './api';
import { getPlaylists as jfGetPlaylists } from './jellyfinApi';
import { getPlaylist as jfGetPlaylist, getPlaylists as jfGetPlaylists } from './jellyfinApi';
import { APIEndpoints, ServerType } from './types';
// prettier-ignore
const endpoints = [
{ id: 'getPlaylist', endpoint: { subsonic: getPlaylist, jellyfin: undefined } },
{ id: 'getPlaylist', endpoint: { subsonic: getPlaylist, jellyfin: jfGetPlaylist } },
{ id: 'getPlaylists', endpoint: { subsonic: getPlaylists, jellyfin: jfGetPlaylists } },
{ id: 'getStarred', endpoint: { subsonic: getStarred, jellyfin: undefined } },
{ id: 'getAlbum', endpoint: { subsonic: getAlbum, jellyfin: undefined } },

115
src/api/jellyfinApi.ts

@ -1,11 +1,13 @@
import axios from 'axios';
import _ from 'lodash';
import { nanoid } from 'nanoid/non-secure';
import { handleDisconnect } from '../components/settings/DisconnectButton';
import { notifyToast } from '../components/shared/toast';
import { Item } from './types';
const getAuth = () => {
return {
userId: localStorage.getItem('username') || '',
username: localStorage.getItem('username') || '',
token: localStorage.getItem('token') || '',
server: localStorage.getItem('server') || '',
deviceId: localStorage.getItem('deviceId') || '',
@ -47,16 +49,109 @@ jellyfinApi.interceptors.response.use(
}
);
export const getPlaylists = async () => {
const params = {
SortBy: 'SortName',
SortOrder: 'Ascending',
IncludeItemTypes: 'Playlist',
Recursive: true,
const getStreamUrl = (id: string) => {
return (
`${API_BASE_URL}/Audio` +
`/${id}` +
`/universal` +
`?UserId=${auth.username}` +
`&DeviceId=${auth.deviceId}` +
`&AudioCodec=aac` +
`&api_key=${auth.token}` +
`&PlaySessionId=${auth.deviceId}` +
`&Container=['opus','mp3','aac','m4a','m4b','flac','wav','ogg']`
);
};
const getCoverArtUrl = (item: any, size?: number) => {
if (!item.ImageTags?.Primary) {
return 'img/placeholder.jpg';
}
return (
`${API_BASE_URL}/Items` +
`/${item.Id}` +
`/Images/Primary` +
`?width=${size}` +
`&height=${size}`
);
};
const normalizeSong = (item: any) => {
return {
id: item.Id,
parent: undefined,
isDir: item.isFolder,
title: item.Name,
album: item.Album,
albumId: item.AlbumId,
artist: item?.ArtistItems[0]?.Name,
artistId: item?.ArtistItems[0]?.Id,
track: item.IndexNumber,
year: item.ProductionYear,
genre: item.GenreItems && item?.GenreItems[0]?.Name,
size: item.MediaSources?.Size,
contentType: undefined,
suffix: undefined,
duration: item.RunTimeTicks / 10000000,
bitRate: item.MediaSources?.MediaStreams?.BitRate,
path: item.Path,
playCount: item.UserData.PlayCount,
discNumber: undefined,
created: item.DateCreated,
streamUrl: getStreamUrl(item.Id),
image: getCoverArtUrl(item, 150),
starred: item.UserData.isFavorite ? 'true' : undefined,
type: Item.Music,
uniqueId: nanoid(),
};
};
const normalizePlaylist = (item: any) => {
return {
id: item.Id,
title: item.Name,
comment: undefined,
owner: undefined,
public: undefined,
songCount: item.SongCount,
duration: item.RunTimeTicks / 10000000,
created: item.DateCreated,
changed: item.DateLastMediaAdded,
image: getCoverArtUrl(item, 350),
type: Item.Playlist,
uniqueId: nanoid(),
song: [],
};
};
export const getPlaylist = async (options: { id: string }) => {
const { data } = await jellyfinApi.get(`/Items`, {
params: { ids: options.id, UserId: auth.username },
});
const { data: songData } = await jellyfinApi.get(`/Playlists/${options.id}/Items`, {
params: { UserId: auth.username },
});
return {
...normalizePlaylist(data.Items[0]),
songCount: songData.Items.length,
song: (songData.Items || []).map((entry: any) => normalizeSong(entry)),
};
};
const playlists: any = await jellyfinApi.get(`Users/${auth.userId}/Items`, { params });
console.log(`playlists`, playlists);
export const getPlaylists = async () => {
const { data } = await jellyfinApi.get(`/Users/${auth.username}/Items`, {
params: {
SortBy: 'SortName',
SortOrder: 'Ascending',
IncludeItemTypes: 'Playlist',
Recursive: true,
},
});
return _.filter(playlists.Items, (item) => item.MediaType === 'Audio');
return (_.filter(data.Items, (item) => item.MediaType === 'Audio') || []).map((entry) =>
normalizePlaylist(entry)
);
};

8
src/api/types.ts

@ -73,12 +73,12 @@ export interface Playlist {
id: string;
title: string;
comment?: string;
owner: string;
owner?: string;
public?: boolean;
songCount: number;
songCount?: number;
duration: number;
created: string;
changed: string;
created?: string;
changed?: string;
image: string;
type: Item.Playlist;
uniqueId: string;

5
src/components/playlist/PlaylistList.tsx

@ -4,7 +4,7 @@ import { useHistory } from 'react-router-dom';
import { Form, Whisper } from 'rsuite';
import settings from 'electron-settings';
import useSearchQuery from '../../hooks/useSearchQuery';
import { createPlaylist, getPlaylists } from '../../api/api';
import { createPlaylist } from '../../api/api';
import ListViewType from '../viewtypes/ListViewType';
import PageLoader from '../loader/PageLoader';
import GenericPage from '../layout/GenericPage';
@ -21,6 +21,7 @@ import {
toggleRangeSelected,
toggleSelected,
} from '../../redux/multiSelectSlice';
import { apiController } from '../../api/controller';
const PlaylistList = () => {
const dispatch = useAppDispatch();
@ -32,7 +33,7 @@ const PlaylistList = () => {
const [newPlaylistName, setNewPlaylistName] = useState('');
const [viewType, setViewType] = useState(settings.getSync('playlistViewType') || 'list');
const { isLoading, isError, data: playlists, error }: any = useQuery(['playlists'], () =>
getPlaylists()
apiController({ serverType: config.serverType, endpoint: 'getPlaylists' })
);
const filteredData = useSearchQuery(misc.searchQuery, playlists, ['title', 'comment', 'owner']);

9
src/components/playlist/PlaylistView.tsx

@ -19,7 +19,6 @@ import {
import {
clearPlaylist,
deletePlaylist,
getPlaylist,
updatePlaylistSongsLg,
updatePlaylistSongs,
updatePlaylist,
@ -76,6 +75,7 @@ import {
} from '../../redux/playlistSlice';
import { PageHeaderSubtitleDataLine } from '../layout/styled';
import CustomTooltip from '../shared/CustomTooltip';
import { apiController } from '../../api/controller';
interface PlaylistParams {
id: string;
@ -94,8 +94,13 @@ const PlaylistView = ({ ...rest }) => {
const { id } = useParams<PlaylistParams>();
const playlistId = rest.id ? rest.id : id;
const { isLoading, isError, data, error }: any = useQuery(['playlist', playlistId], () =>
getPlaylist(playlistId)
apiController({
serverType: config.serverType,
endpoint: 'getPlaylist',
args: { id: playlistId },
})
);
const [customPlaylistImage, setCustomPlaylistImage] = useState<string | string[]>(
'img/placeholder.jpg'
);

12
src/shared/utils.ts

@ -112,14 +112,8 @@ const formatBytes = (bytes: number, decimals = 2) => {
export const formatSongDuration = (duration: number) => {
const hours = Math.floor(duration / 60 / 60);
const minutes = Math.floor((duration / 60) % 60);
const seconds = String(duration % 60).padStart(2, '0');
// if (minutes > 60) {
// const hours = Math.floor(minutes / 60);
// const newMinutes = Math.floor(minutes % 60);
// const newSeconds = String(duration % 60).padStart(2, '0');
// return `${hours}:${newMinutes}:${newSeconds}`;
// }
const seconds = String(Math.trunc(Number(duration % 60))).padStart(2, '0');
if (hours > 0) {
return `${hours}:${String(minutes).padStart(2, '0')}:${seconds}`;
}
@ -134,7 +128,7 @@ export const formatSongDuration = (duration: number) => {
export const formatDuration = (duration: number) => {
const hours = Math.floor(duration / 60 / 60);
const minutes = Math.floor((duration / 60) % 60);
const seconds = String(duration % 60).padStart(2, '0');
const seconds = String(Math.trunc(Number(duration % 60))).padStart(2, '0');
if (hours > 0) {
return `${hours} hr ${minutes} min ${seconds} sec`;

Loading…
Cancel
Save