Skip to content

Commit

Permalink
Feature: Resizable Album Art + Albumartist Respect (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
johnnyshankman authored Sep 27, 2024
1 parent aac1376 commit 3994775
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 122 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,8 @@ This project was built with the following technologies:
- [x] Ability to deduplicate identical songs in library easily
- [x] Ability to delete entire albums of songs from library and filestystem
- [x] Adjustable column widths for songname, artist, and album
- [ ] Sort by albumartist not the plain old artist, pretty much only used by rap albums with features (2Pac - All Eyez On Me)
- [x] Sort by albumartist not the plain old artist, pretty much only used by rap albums with features (2Pac - All Eyez On Me)
- [x] Adjustable album art size
- [ ] Edit song metadata
- [ ] Show stats about your library somewhere, like GB and # of songs
- [ ] Hide and show columns in the explorer
Expand Down
1 change: 1 addition & 0 deletions src/common/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type LightweightAudioMetadata = {
title?: ICommonTagsResult['title'];
track?: ICommonTagsResult['track'];
disk: ICommonTagsResult['disk'];
albumartist?: ICommonTagsResult['albumartist'];
/**
* @important we never store the picture in the user config because it
* overloads the IPC and causes crashes. we always request it song by song
Expand Down
153 changes: 65 additions & 88 deletions src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,32 @@ async function sortFilesByQuality(files: string[]) {
return sorted.map((item) => item.file);
}

const getArtist = (metadata: LightweightAudioMetadata) => {
return (metadata.common?.albumartist || metadata.common?.artist || '')
.toLowerCase()
.replace(/^the /, '');
};

const compareMetadata = (
a: LightweightAudioMetadata,
b: LightweightAudioMetadata,
) => {
const artistA = getArtist(a);
const artistB = getArtist(b);
const albumA = a.common?.album?.toLowerCase() || '';
const albumB = b.common?.album?.toLowerCase() || '';
const diskA = a.common?.disk?.no || 0;
const diskB = b.common?.disk?.no || 0;
const trackA = a.common?.track?.no || 0;
const trackB = b.common?.track?.no || 0;

if (artistA !== artistB) return artistA.localeCompare(artistB);
if (albumA !== albumB) return albumA.localeCompare(albumB);
if (diskA !== diskB) return diskA - diskB;
if (trackA !== trackB) return trackA - trackB;
return 0;
};

const hideOrDeleteDupes = async (event: IpcMainEvent, del: boolean) => {
const userConfig = getUserConfig();
const { library } = userConfig;
Expand Down Expand Up @@ -326,12 +352,12 @@ ipcMain.on('delete-album', async (event, arg) => {
const userConfig = getUserConfig();
const { song } = arg;

const { artist } = userConfig.library[song].common;
const { album } = userConfig.library[song].common;
const { artist, album, albumartist } = userConfig.library[song].common;

const songsToDelete = Object.keys(userConfig.library).filter(
(key) =>
userConfig.library[key].common.artist === artist &&
(userConfig.library[key].common.artist === artist ||
userConfig.library[key].common.albumartist === albumartist) &&
userConfig.library[key].common.album === album,
);

Expand Down Expand Up @@ -497,6 +523,7 @@ ipcMain.on('add-to-library', async (event): Promise<any> => {
title: metadata.common.title,
track: metadata.common.track,
disk: metadata.common.disk,
albumartist: metadata.common.albumartist,
/**
* purposely do not store the picture data in the user's config
* because it's too large and we can lazy load it when needed
Expand Down Expand Up @@ -534,39 +561,7 @@ ipcMain.on('add-to-library', async (event): Promise<any> => {
// sort filesToTags by artist, album, then track number
const orderedFilesToTags: { [key: string]: LightweightAudioMetadata } = {};
Object.keys(filesToTags)
.sort((a, b) => {
const artistA = filesToTags[a].common?.artist
?.toLowerCase()
.replace(/^the /, '');
const artistB = filesToTags[b].common?.artist
?.toLowerCase()
.replace(/^the /, '');
const albumA = filesToTags[a].common?.album?.toLowerCase();
const albumB = filesToTags[b].common?.album?.toLowerCase();
const trackA = filesToTags[a].common?.track?.no;
const trackB = filesToTags[b].common?.track?.no;
const diskA = filesToTags[a].common?.disk?.no;
const diskB = filesToTags[b].common?.disk?.no;

if (!artistA) return -1;
if (!artistB) return 1;
if (!albumA) return -1;
if (!albumB) return 1;
if (!trackA) return -1;
if (!trackB) return 1;

if (artistA < artistB) return -1;
if (artistA > artistB) return 1;
if (albumA < albumB) return -1;
if (albumA > albumB) return 1;
if (diskA && diskB) {
if (diskA < diskB) return -1;
if (diskA > diskB) return 1;
}
if (trackA < trackB) return -1;
if (trackA > trackB) return 1;
return 0;
})
.sort((a, b) => compareMetadata(filesToTags[a], filesToTags[b]))
.forEach((key) => {
orderedFilesToTags[key] = filesToTags[key];
});
Expand Down Expand Up @@ -667,6 +662,7 @@ ipcMain.on(
title: metadata.common.title,
track: metadata.common.track,
disk: metadata.common.disk,
albumartist: metadata.common.albumartist,
/**
* purposely do not store the picture data in the user's config
* because it's too large and we can lazy load it when needed
Expand Down Expand Up @@ -709,42 +705,9 @@ ipcMain.on(
});
}

// sort filesToTags by artist, album, then track number
const orderedFilesToTags: { [key: string]: LightweightAudioMetadata } = {};
Object.keys(filesToTags)
.sort((a, b) => {
const artistA = filesToTags[a].common?.artist
?.toLowerCase()
.replace(/^the /, '');
const artistB = filesToTags[b].common?.artist
?.toLowerCase()
.replace(/^the /, '');
const albumA = filesToTags[a].common?.album?.toLowerCase();
const albumB = filesToTags[b].common?.album?.toLowerCase();
const trackA = filesToTags[a].common?.track?.no;
const trackB = filesToTags[b].common?.track?.no;
const diskA = filesToTags[a].common?.disk?.no;
const diskB = filesToTags[b].common?.disk?.no;

if (!artistA) return -1;
if (!artistB) return 1;
if (!albumA) return -1;
if (!albumB) return 1;
if (!trackA) return -1;
if (!trackB) return 1;

if (artistA < artistB) return -1;
if (artistA > artistB) return 1;
if (albumA < albumB) return -1;
if (albumA > albumB) return 1;
if (diskA && diskB) {
if (diskA < diskB) return -1;
if (diskA > diskB) return 1;
}
if (trackA < trackB) return -1;
if (trackA > trackB) return 1;
return 0;
})
.sort((a, b) => compareMetadata(filesToTags[a], filesToTags[b]))
.forEach((key) => {
orderedFilesToTags[key] = filesToTags[key];
});
Expand Down Expand Up @@ -971,7 +934,7 @@ const createWindow = async () => {

mainWindow.loadURL(resolveHtmlPath('index.html'));

mainWindow.webContents.on('did-finish-load', () => {
mainWindow.webContents.on('did-finish-load', async () => {
if (!mainWindow) {
throw new Error('"mainWindow" is not defined');
}
Expand All @@ -980,28 +943,42 @@ const createWindow = async () => {

/**
* @important shim any missing data from updates between versions of app
* 1. add lastPlayed to all songs and save it back to the userConfig.json file
* 2. TBD
* 1. give all songs an initial additionalInfo object if it is missing
* 2. if the additionalInfo has no dateAdded, make it the current timestamp
* 3. give all songs an albumartist if it is missing
*/
if (userConfig.library) {
Object.keys(userConfig.library).forEach((key) => {
const song = {
...userConfig.library[key],
const keys = Object.keys(userConfig.library);
for (let i = 0; i < keys.length; i += 1) {
const key = keys[i];
const song = {
...userConfig.library[key],
};

if (song.additionalInfo === undefined) {
song.additionalInfo = {
playCount: 0,
lastPlayed: 0,
dateAdded: Date.now(),
};
if (song.additionalInfo === undefined) {
song.additionalInfo = {
playCount: 0,
lastPlayed: 0,
dateAdded: Date.now(),
};
}
}

if (song.additionalInfo.dateAdded === undefined) {
song.additionalInfo.dateAdded = Date.now();
if (song.additionalInfo.dateAdded === undefined) {
song.additionalInfo.dateAdded = Date.now();
}

if (song.common.albumartist === undefined) {
// eslint-disable-next-line no-await-in-loop
const metadata = await mm.parseFile(key);
if (metadata.common.albumartist) {
song.common.albumartist = metadata.common.albumartist;
} else {
// keeps us from having to do this check again in the future
// blank string is still falsey in JS so this won't affect sorting
song.common.albumartist = '';
}
}

userConfig.library[key] = song;
});
userConfig.library[key] = song;
}

writeFileSyncToUserConfig(userConfig);
Expand Down
6 changes: 3 additions & 3 deletions src/renderer/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { MemoryRouter as Router, Routes, Route } from 'react-router-dom';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import MainDash from './components/MainDash';
import Main from './components/Main';
import './App.scss';
/**
* @dev forces all google material ui components to use the dark theme
Expand All @@ -21,12 +21,12 @@ export default function App() {
element={
<div className="shell">
{/**
* @dev themeprovder and cssbaseline are used to render
* @dev themeprovider and cssbaseline are used to render
* all google material ui components in dark mode
*/}
<ThemeProvider theme={darkTheme}>
<CssBaseline />
<MainDash />
<Main />
</ThemeProvider>
</div>
}
Expand Down
28 changes: 16 additions & 12 deletions src/renderer/components/LibraryList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -247,27 +247,31 @@ export default function LibraryList({
break;
case 'artist':
filtered = Object.keys(filteredLibrary).sort((a, b) => {
const artistA = filteredLibrary[a].common?.artist
?.toLowerCase()
.replace(/^the /, '');
const artistB = filteredLibrary[b].common?.artist
?.toLowerCase()
.replace(/^the /, '');
const artistA =
filteredLibrary[a].common?.albumartist ||
filteredLibrary[a].common?.artist;
const artistB =
filteredLibrary[b].common?.albumartist ||
filteredLibrary[b].common?.artist;
const normalizedArtistA = artistA?.toLowerCase().replace(/^the /, '');
const normalizedArtistB = artistB?.toLowerCase().replace(/^the /, '');
const albumA = filteredLibrary[a].common?.album?.toLowerCase();
const albumB = filteredLibrary[b].common?.album?.toLowerCase();
const trackA = filteredLibrary[a].common?.track?.no;
const trackB = filteredLibrary[b].common?.track?.no;
// handle null cases
if (!artistA) return filterDirection === 'asc' ? -1 : 1;
if (!artistB) return filterDirection === 'asc' ? 1 : -1;
if (!normalizedArtistA) return filterDirection === 'asc' ? -1 : 1;
if (!normalizedArtistB) return filterDirection === 'asc' ? 1 : -1;

if (!albumA) return -1;
if (!albumB) return 1;
if (!trackA) return -1;
if (!trackB) return 1;

if (artistA < artistB) return filterDirection === 'asc' ? -1 : 1;
if (artistA > artistB) return filterDirection === 'asc' ? 1 : -1;
if (normalizedArtistA < normalizedArtistB)
return filterDirection === 'asc' ? -1 : 1;
if (normalizedArtistA > normalizedArtistB)
return filterDirection === 'asc' ? 1 : -1;

if (albumA < albumB) return -1;
if (albumA > albumB) return 1;
Expand Down Expand Up @@ -531,9 +535,9 @@ export default function LibraryList({
{/**
* @dev since the list could be 1000s of songs long we must virtualize it
*/}
{hasSongs && initialized && (
{initialized && width && hasSongs && (
<List
width={width || 0}
width={width}
height={rowContainerHeight}
rowRenderer={renderSongRow}
rowCount={Object.keys(filteredLibrary || {}).length}
Expand Down
6 changes: 2 additions & 4 deletions src/renderer/components/LinearProgressBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export default function LinearProgressBar({
justifyContent: 'center',
}}
>
<Tooltip title="Scroll to song">
<Tooltip title="Scroll to song" placement="top" arrow>
<LessOpaqueTinyText
sx={{
margin: 0,
Expand Down Expand Up @@ -122,9 +122,7 @@ export default function LinearProgressBar({
{convertToMMSS(position)}
</LessOpaqueTinyText>

<Tooltip
title={`Scroll to ${currentSongMetadata.common?.artist || 'artist'}`}
>
<Tooltip title="Scroll to artist" placement="top" arrow>
<LessOpaqueTinyText
sx={{
margin: 0,
Expand Down
Loading

0 comments on commit 3994775

Please sign in to comment.