import * as odd from '@oddjs/odd';
import { getRecoil, setRecoil } from 'recoil-nexus';
import type PublicFile from '@oddjs/odd/fs/v1/PublicFile';
import type PrivateFile from '@oddjs/odd/fs/v1/PrivateFile';
import { isFile } from '@oddjs/odd/fs/types/check';
import { filesystemStore } from '../../../stores';
import { collectionStore, AREAS } from '../stores';
import { addNotification } from '../../../lib/notifications';
import { fileToUint8Array } from '../../../lib/utils';
export type PlaylistSourceType =
| 'apple'
| 'spotify'
| 'youtube'
| 'tidal'
| 'deezer'
| 'bandcamp'
| 'soundcloud'
| 'other';
export type PlaylistSource = {
type: PlaylistSourceType;
url: string;
};
export type Playlist = {
cid: string;
ctime: number;
name: string;
private: boolean;
size: number;
src: string;
cover?: string;
sources?: PlaylistSource[];
};
export type COLLECTION = {
publicPlaylists: Playlist[];
privatePlaylists: Playlist[];
selectedArea: AREAS;
loading: boolean;
};
type Link = {
size: number;
};
export const COLLECTION_DIRS = {
[AREAS.PUBLIC]: odd.path.directory('public', 'collection'),
[AREAS.PRIVATE]: odd.path.directory('private', 'collection'),
};
const FILE_SIZE_LIMIT = 20;
/**
* Get playlists from the user's WNFS and construct the `src` value for the playlists
*/
export const getPlaylistsFromWNFS: () => Promise<void> = async () => {
const collection = getRecoil(collectionStore);
const fs = getRecoil(filesystemStore);
if (!fs) return;
try {
// Set loading: true on the collectionStore
setRecoil(collectionStore, { ...collection, loading: true });
const { selectedArea } = collection;
const isPrivate = selectedArea === AREAS.PRIVATE;
// Set path to either private or public gallery dir
const path = COLLECTION_DIRS[selectedArea];
// Get list of links for files in the gallery dir
const links = await fs.ls(path);
let playlists = await Promise.all(
Object.entries(links).map(async ([name]) => {
const file = await fs.get(
odd.path.combine(
COLLECTION_DIRS[selectedArea],
odd.path.file(`${name}`)
)
);
if (!isFile(file)) return null;
// The CID for private files is currently located in `file.header.content`,
// whereas the CID for public files is located in `file.cid`
const cid = isPrivate
? (file as PrivateFile).header.content.toString()
: (file as PublicFile).cid.toString();
// Create a blob to use as the playlist `src`
const blob = new Blob([file.content]);
const src = URL.createObjectURL(blob);
const ctime = isPrivate
? (file as PrivateFile).header.metadata.unixMeta.ctime
: (file as PublicFile).header.metadata.unixMeta.ctime;
return {
cid,
ctime,
name,
private: isPrivate,
size: (links[name] as Link).size,
src,
};
})
);
// Sort playlists by ctime(created at date)
// NOTE: this will eventually be controlled via the UI
playlists = playlists.filter((a) => !!a);
playlists.sort((a, b) => b.ctime - a.ctime);
// Push playlists to the collectionStore
setRecoil(collectionStore, {
...collection,
...(isPrivate
? {
privatePlaylists: playlists,
}
: {
publicPlaylists: playlists,
}),
loading: false,
});
} catch (error) {
setRecoil(collectionStore, {
...collection,
loading: false,
});
}
};
/**
* Upload a playlist to the user's private or public WNFS
* @param playlist
*/
export const uploadPlaylistToWNFS: (playlist: File) => Promise<void> = async (
playlist
) => {
const collection = getRecoil(collectionStore);
const fs = getRecoil(filesystemStore);
if (!fs) return;
try {
const { selectedArea } = collection;
// Reject files over 20MB
const playlistSizeInMB = playlist.size / (1024 * 1024);
if (playlistSizeInMB > FILE_SIZE_LIMIT) {
throw new Error('Playlist can be no larger than 20MB');
}
// Reject the upload if the playlist already exists in the directory
const playlistExists = await fs.exists(
odd.path.combine(
COLLECTION_DIRS[selectedArea],
odd.path.file(playlist.name)
)
);
if (playlistExists) {
throw new Error(`${playlist.name} playlist already exists`);
}
// Create a sub directory and add some content
await fs.write(
odd.path.combine(
COLLECTION_DIRS[selectedArea],
odd.path.file(playlist.name)
),
await fileToUint8Array(playlist)
);
// Announce the changes to the server
await fs.publish();
addNotification({
msg: `${playlist.name} playlist has been published`,
type: 'success',
});
} catch (error) {
addNotification({ msg: (error as Error).message, type: 'error' });
console.error(error);
}
};
/**
* Delete a playlist from the user's private or public WNFS
* @param name
*/
export const deletePlaylistFromWNFS: (name: string) => Promise<void> = async (
name
) => {
const collection = getRecoil(collectionStore);
const fs = getRecoil(filesystemStore);
if (!fs) return;
try {
const { selectedArea } = collection;
const playlistExists = await fs.exists(
odd.path.combine(COLLECTION_DIRS[selectedArea], odd.path.file(name))
);
if (playlistExists) {
// Remove playlists from server
await fs.rm(
odd.path.combine(COLLECTION_DIRS[selectedArea], odd.path.file(name))
);
// Announce the changes to the server
await fs.publish();
addNotification({
msg: `${name} playlist has been deleted`,
type: 'success',
});
// Refetch playlists and update collectionStore
await getPlaylistsFromWNFS();
} else {
throw new Error(`${name} playlist has already been deleted`);
}
} catch (error) {
addNotification({ msg: (error as Error).message, type: 'error' });
console.error(error);
}
};
/**
* Handle uploads made by interacting with the file input directly
*/
export const handleFileInput: (
files: FileList | null
) => Promise<void> = async (files) => {
if (!files) return;
await Promise.all(
Array.from(files).map(async (file) => {
await uploadPlaylistToWNFS(file);
})
);
// Refetch playlists and update collectionStore
await getPlaylistsFromWNFS();
};