diff --git a/client/src/components/MainWindow.tsx b/client/src/components/MainWindow.tsx
index d3b73f1..46153ca 100644
--- a/client/src/components/MainWindow.tsx
+++ b/client/src/components/MainWindow.tsx
@@ -9,6 +9,7 @@ import ArtistWindow from './windows/ArtistWindow';
import AlbumWindow from './windows/AlbumWindow';
import TagWindow from './windows/TagWindow';
import SongWindow from './windows/SongWindow';
+import StatisticsWindow from './windows/StatisticsWindow';
var _ = require('lodash');
const darkTheme = createMuiTheme({
@@ -76,6 +77,7 @@ export default function MainWindow(props: any) {
newWindowState[WindowType.Album](),
newWindowState[WindowType.Artist](),
newWindowState[WindowType.Tag](),
+ newWindowState[WindowType.Statistics](),
],
tabReducers: [
newWindowReducer[WindowType.Query],
@@ -83,6 +85,7 @@ export default function MainWindow(props: any) {
newWindowReducer[WindowType.Album],
newWindowReducer[WindowType.Artist],
newWindowReducer[WindowType.Tag],
+ newWindowReducer[WindowType.Statistics],
],
tabTypes: [
WindowType.Query,
@@ -90,6 +93,7 @@ export default function MainWindow(props: any) {
WindowType.Album,
WindowType.Artist,
WindowType.Tag,
+ WindowType.Statistics,
],
activeTab: 0
})
@@ -134,6 +138,12 @@ export default function MainWindow(props: any) {
dispatch={tabDispatch}
mainDispatch={dispatch}
/>
+ case WindowType.Statistics:
+ return
default:
throw new Error("Unimplemented window type");
}
diff --git a/client/src/components/appbar/AddTabMenu.tsx b/client/src/components/appbar/AddTabMenu.tsx
index b001fe1..be3315c 100644
--- a/client/src/components/appbar/AddTabMenu.tsx
+++ b/client/src/components/appbar/AddTabMenu.tsx
@@ -28,5 +28,13 @@ export default function AddTabMenu(props: IProps) {
})
}}
>{WindowType.Query}
+
}
\ No newline at end of file
diff --git a/client/src/components/common/StoreLinkIcon.tsx b/client/src/components/common/StoreLinkIcon.tsx
index 7359ab0..90697e6 100644
--- a/client/src/components/common/StoreLinkIcon.tsx
+++ b/client/src/components/common/StoreLinkIcon.tsx
@@ -1,26 +1,16 @@
import React from 'react';
import { ReactComponent as GPMIcon } from '../../assets/googleplaymusic_icon.svg';
-
-export enum ExternalStore {
- GooglePlayMusic = "GPM",
-}
+import { MusicStore } from '../../lib/MusicStore';
export interface IProps {
- whichStore: ExternalStore,
-}
-
-export function whichStore(url: string) {
- if(url.includes('play.google.com')) {
- return ExternalStore.GooglePlayMusic;
- }
- return undefined;
+ whichStore: MusicStore,
}
export default function StoreLinkIcon(props: any) {
const { whichStore, ...restProps } = props;
switch(whichStore) {
- case ExternalStore.GooglePlayMusic:
+ case MusicStore.GooglePlayMusic:
return ;
default:
throw new Error("Unknown external store: " + whichStore)
diff --git a/client/src/components/tables/ResultsTable.tsx b/client/src/components/tables/ResultsTable.tsx
index cce223f..434821e 100644
--- a/client/src/components/tables/ResultsTable.tsx
+++ b/client/src/components/tables/ResultsTable.tsx
@@ -7,18 +7,7 @@ import PersonIcon from '@material-ui/icons/Person';
import AlbumIcon from '@material-ui/icons/Album';
import AudiotrackIcon from '@material-ui/icons/Audiotrack';
import LocalOfferIcon from '@material-ui/icons/LocalOffer';
-import { songGetters } from '../../lib/songGetters';
-
-export interface SongGetters {
- getTitle: (song: any) => string,
- getId: (song: any) => number,
- getArtistNames: (song: any) => string[],
- getArtistIds: (song: any) => number[],
- getAlbumNames: (song: any) => string[],
- getAlbumIds: (song: any) => number[],
- getTagNames: (song: any) => string[][], // Each tag is represented as a series of strings.
- getTagIds: (song: any) => number[][], // Each tag is represented as a series of ids.
-}
+import { songGetters, SongGetters } from '../../lib/songGetters';
export interface IProps {
songs: any[],
diff --git a/client/src/components/windows/AlbumWindow.tsx b/client/src/components/windows/AlbumWindow.tsx
index 6cefcb8..658eb8e 100644
--- a/client/src/components/windows/AlbumWindow.tsx
+++ b/client/src/components/windows/AlbumWindow.tsx
@@ -3,11 +3,13 @@ import { Box, Typography, IconButton, CircularProgress } from '@material-ui/core
import AlbumIcon from '@material-ui/icons/Album';
import * as serverApi from '../../api';
import { WindowState } from './Windows';
-import StoreLinkIcon, { whichStore } from '../common/StoreLinkIcon';
+import StoreLinkIcon from '../common/StoreLinkIcon';
import EditableText from '../common/EditableText';
import SubmitChangesButton from '../common/SubmitChangesButton';
-import SongTable, { SongGetters } from '../tables/ResultsTable';
+import SongTable from '../tables/ResultsTable';
import { saveAlbumChanges } from '../../lib/saveChanges';
+import { whichStore } from '../../lib/MusicStore';
+import { SongGetters } from '../../lib/songGetters';
var _ = require('lodash');
export type AlbumMetadata = serverApi.AlbumDetails;
diff --git a/client/src/components/windows/ArtistWindow.tsx b/client/src/components/windows/ArtistWindow.tsx
index 7b8ed4c..afea3b6 100644
--- a/client/src/components/windows/ArtistWindow.tsx
+++ b/client/src/components/windows/ArtistWindow.tsx
@@ -3,11 +3,13 @@ import { Box, Typography, IconButton, Button, CircularProgress } from '@material
import PersonIcon from '@material-ui/icons/Person';
import * as serverApi from '../../api';
import { WindowState } from './Windows';
-import StoreLinkIcon, { whichStore } from '../common/StoreLinkIcon';
+import StoreLinkIcon from '../common/StoreLinkIcon';
import EditableText from '../common/EditableText';
import SubmitChangesButton from '../common/SubmitChangesButton';
-import SongTable, { SongGetters } from '../tables/ResultsTable';
+import SongTable from '../tables/ResultsTable';
import { saveArtistChanges } from '../../lib/saveChanges';
+import { whichStore } from '../../lib/MusicStore';
+import { SongGetters } from '../../lib/songGetters';
var _ = require('lodash');
export type ArtistMetadata = serverApi.ArtistDetails;
diff --git a/client/src/components/windows/SongWindow.tsx b/client/src/components/windows/SongWindow.tsx
index bf69c84..dffc371 100644
--- a/client/src/components/windows/SongWindow.tsx
+++ b/client/src/components/windows/SongWindow.tsx
@@ -7,10 +7,11 @@ import * as serverApi from '../../api';
import { WindowState } from './Windows';
import { ArtistMetadata } from './ArtistWindow';
import { AlbumMetadata } from './AlbumWindow';
-import StoreLinkIcon, { whichStore } from '../common/StoreLinkIcon';
+import StoreLinkIcon from '../common/StoreLinkIcon';
import EditableText from '../common/EditableText';
import SubmitChangesButton from '../common/SubmitChangesButton';
import { saveSongChanges } from '../../lib/saveChanges';
+import { whichStore } from '../../lib/MusicStore';
export type SongMetadata = serverApi.SongDetails;
export type SongMetadataChanges = serverApi.ModifySongRequest;
diff --git a/client/src/components/windows/StatisticsWindow.tsx b/client/src/components/windows/StatisticsWindow.tsx
new file mode 100644
index 0000000..4b167b9
--- /dev/null
+++ b/client/src/components/windows/StatisticsWindow.tsx
@@ -0,0 +1,251 @@
+import React, { useEffect, useState } from 'react';
+import { WindowState } from "./Windows"
+import { MusicStore, whichStore } from '../../lib/MusicStore';
+import { songGetters, SongGetters } from '../../lib/songGetters';
+import { Typography, Box, CircularProgress, Paper } from '@material-ui/core';
+import EqualizerIcon from '@material-ui/icons/Equalizer';
+import * as serverApi from '../../api';
+
+var _ = require('lodash');
+
+export interface SongStatistics {
+ numberOf: number,
+ noAlbum: number,
+ noArtist: number,
+ perStore: Record,
+}
+
+export interface AlbumStatistics {
+ numberOf: number,
+ noSongs: number,
+ noArtist: number,
+ perStore: Record,
+}
+
+export interface ArtistStatistics {
+ numberOf: number,
+ noSongs: number,
+ noAlbums: number,
+ perStore: Record,
+}
+
+export interface TagStatistics {
+ numberOf: number,
+ noItems: number,
+}
+
+
+export interface Statistics {
+ songStats: SongStatistics,
+ artistStats: ArtistStatistics,
+ albumStats: AlbumStatistics,
+ tagStats: TagStatistics,
+}
+export function newStatistics(): Statistics {
+ return {
+ songStats: {
+ numberOf: 0,
+ noAlbum: 0,
+ noArtist: 0,
+ perStore: {
+ [MusicStore.GooglePlayMusic]: 0
+ }
+ },
+ artistStats: {
+ numberOf: 0,
+ noAlbums: 0,
+ noSongs: 0,
+ perStore: {
+ [MusicStore.GooglePlayMusic]: 0
+ }
+ },
+ albumStats: {
+ numberOf: 0,
+ noArtist: 0,
+ noSongs: 0,
+ perStore: {
+ [MusicStore.GooglePlayMusic]: 0
+ }
+ },
+ tagStats: {
+ numberOf: 0,
+ noItems: 0,
+ }
+ };
+}
+
+export interface StatisticsWindowState extends WindowState {
+ stats: Statistics | null,
+ retrievingStats: boolean,
+}
+
+export enum StatisticsWindowStateActions {
+ Reset = "Reset",
+ AddSongs = "AddSongs",
+ SetRetrieving = "SetRetrieving",
+}
+
+export function StatisticsWindowReducer(state: StatisticsWindowState, action: any) {
+ switch (action.type) {
+ case StatisticsWindowStateActions.Reset:
+ return {
+ ...state,
+ stats: null,
+ retrievingStats: true,
+ };
+ case StatisticsWindowStateActions.AddSongs:
+ return {
+ ...state,
+ stats: addSongs(state.stats, action.songs, songGetters)
+ };
+ case StatisticsWindowStateActions.SetRetrieving:
+ return {
+ ...state,
+ retrievingStats: action.value,
+ };
+ default:
+ throw new Error("Unimplemented SongWindow state update.")
+ }
+}
+
+export function addSongs(stats: Statistics | null, songs: any[], songGetters: SongGetters) {
+ var r: Statistics = stats ? _.cloneDeep(stats) : newStatistics();
+
+ songs.forEach((song: any) => {
+ r.songStats.numberOf++;
+ if (songGetters.getAlbumIds(song) === []) { r.songStats.noAlbum++; }
+ if (songGetters.getArtistIds(song) === []) { r.songStats.noArtist++; }
+ songGetters.getStoreLinks(song).forEach((link: string) => {
+ const which = whichStore(link);
+ if (which) {
+ r.songStats.perStore[which]++;
+ }
+ })
+ })
+
+ return r;
+}
+
+export async function gatherStats(dispatch: (action: any) => void) {
+ // Gather 50 songs at a time
+ for (let i = 0; ; i += 50) {
+ var q: serverApi.QueryRequest = {
+ query: {},
+ offsetsLimits: {
+ songOffset: i,
+ songLimit: 50,
+ },
+ ordering: {
+ orderBy: {
+ type: serverApi.OrderByType.Name,
+ },
+ ascending: true,
+ },
+ };
+
+ const requestOpts = {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(q),
+ };
+ const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts)
+ let json: any = await response.json();
+ const songs = json.songs;
+
+ if (!songs.length || songs.length === 0) {
+ // No more songs.
+ break;
+ }
+ dispatch({
+ type: StatisticsWindowStateActions.AddSongs,
+ songs: songs,
+ })
+ }
+}
+
+export function StatisticsDisplay(props: { stats: Statistics }) {
+ return
+
+
+
+ Songs
+ Total: {props.stats.songStats.numberOf}
+ Without album: {props.stats.songStats.noAlbum}
+ Without artist: {props.stats.songStats.noArtist}
+ Linked to Google Play Music: {props.stats.songStats.perStore[MusicStore.GooglePlayMusic]}
+
+
+
+
+
+
+ Artists
+ Total: {props.stats.artistStats.numberOf}
+ Without album: {props.stats.artistStats.noAlbums}
+ Without songs: {props.stats.artistStats.noSongs}
+ Linked to Google Play Music: {props.stats.artistStats.perStore[MusicStore.GooglePlayMusic]}
+
+
+
+
+
+
+ Albums
+ Total: {props.stats.albumStats.numberOf}
+ Without album: {props.stats.albumStats.noArtist}
+ Without songs: {props.stats.albumStats.noSongs}
+ Linked to Google Play Music: {props.stats.albumStats.perStore[MusicStore.GooglePlayMusic]}
+
+
+
+
+
+
+ Tags
+ Total: {props.stats.tagStats.numberOf}
+ Unused: {props.stats.tagStats.noItems}
+
+
+
+
+}
+
+export interface IProps {
+ state: StatisticsWindowState,
+ dispatch: (action: any) => void,
+ mainDispatch: (action: any) => void,
+}
+
+export default function StatisticsWindow(props: IProps) {
+ useEffect(() => {
+ if (!props.state.retrievingStats) return;
+
+ props.dispatch({
+ type: StatisticsWindowStateActions.Reset
+ })
+ gatherStats(props.dispatch).then(() => {
+ props.dispatch({
+ type: StatisticsWindowStateActions.SetRetrieving,
+ value: false,
+ })
+ })
+ }, [props.state.retrievingStats])
+
+ return
+
+
+
+
+
+ {props.state.stats && }
+
+
+}
\ No newline at end of file
diff --git a/client/src/components/windows/TagWindow.tsx b/client/src/components/windows/TagWindow.tsx
index 676aef5..0c879be 100644
--- a/client/src/components/windows/TagWindow.tsx
+++ b/client/src/components/windows/TagWindow.tsx
@@ -3,11 +3,13 @@ import { Box, Typography, IconButton, CircularProgress } from '@material-ui/core
import LocalOfferIcon from '@material-ui/icons/LocalOffer';
import * as serverApi from '../../api';
import { WindowState } from './Windows';
-import StoreLinkIcon, { whichStore } from '../common/StoreLinkIcon';
+import StoreLinkIcon from '../common/StoreLinkIcon';
import EditableText from '../common/EditableText';
import SubmitChangesButton from '../common/SubmitChangesButton';
-import SongTable, { SongGetters } from '../tables/ResultsTable';
+import SongTable from '../tables/ResultsTable';
import { saveTagChanges } from '../../lib/saveChanges';
+import { whichStore } from '../../lib/MusicStore';
+import { SongGetters } from '../../lib/songGetters';
var _ = require('lodash');
export interface FullTagMetadata extends serverApi.TagDetails {
diff --git a/client/src/components/windows/Windows.tsx b/client/src/components/windows/Windows.tsx
index d5840e5..e6c1951 100644
--- a/client/src/components/windows/Windows.tsx
+++ b/client/src/components/windows/Windows.tsx
@@ -4,11 +4,13 @@ import { ArtistWindowReducer } from "./ArtistWindow";
import SearchIcon from '@material-ui/icons/Search';
import PersonIcon from '@material-ui/icons/Person';
import AlbumIcon from '@material-ui/icons/Album';
+import EqualizerIcon from '@material-ui/icons/Equalizer';
import LocalOfferIcon from '@material-ui/icons/LocalOffer';
import AudiotrackIcon from '@material-ui/icons/Audiotrack';
import { SongWindowReducer } from './SongWindow';
import { AlbumWindowReducer } from './AlbumWindow';
import { TagWindowReducer } from './TagWindow';
+import { StatisticsWindowReducer } from './StatisticsWindow';
import { songGetters } from '../../lib/songGetters';
export enum WindowType {
@@ -17,6 +19,7 @@ export enum WindowType {
Album = "Album",
Tag = "Tag",
Song = "Song",
+ Statistics = "Statistics",
}
export interface WindowState {
@@ -29,6 +32,7 @@ export const newWindowReducer = {
[WindowType.Album]: AlbumWindowReducer,
[WindowType.Song]: SongWindowReducer,
[WindowType.Tag]: TagWindowReducer,
+ [WindowType.Statistics]: StatisticsWindowReducer,
}
export const newWindowState = {
@@ -78,4 +82,11 @@ export const newWindowState = {
songsWithTag: null,
}
},
+ [WindowType.Statistics]: () => {
+ return {
+ tabLabel: <>Statistics>,
+ stats: null,
+ retrievingStats: true,
+ }
+ }
}
\ No newline at end of file
diff --git a/client/src/lib/MusicStore.tsx b/client/src/lib/MusicStore.tsx
new file mode 100644
index 0000000..7a9a638
--- /dev/null
+++ b/client/src/lib/MusicStore.tsx
@@ -0,0 +1,9 @@
+export enum MusicStore {
+ GooglePlayMusic = "GPM",
+}
+export function whichStore(url: string) {
+ if(url.includes('play.google.com')) {
+ return MusicStore.GooglePlayMusic;
+ }
+ return undefined;
+}
\ No newline at end of file
diff --git a/client/src/lib/songGetters.tsx b/client/src/lib/songGetters.tsx
index 4f75cc9..ef2d660 100644
--- a/client/src/lib/songGetters.tsx
+++ b/client/src/lib/songGetters.tsx
@@ -1,4 +1,16 @@
-export const songGetters = {
+export interface SongGetters {
+ getTitle: (song: any) => string,
+ getId: (song: any) => number,
+ getArtistNames: (song: any) => string[],
+ getArtistIds: (song: any) => number[],
+ getAlbumNames: (song: any) => string[],
+ getAlbumIds: (song: any) => number[],
+ getTagNames: (song: any) => string[][], // Each tag is represented as a series of strings.
+ getTagIds: (song: any) => number[][], // Each tag is represented as a series of ids.
+ getStoreLinks: (song: any) => string[],
+}
+
+export const songGetters: SongGetters = {
getTitle: (song: any) => song.title,
getId: (song: any) => song.songId,
getArtistNames: (song: any) => song.artists.map((a: any) => a.name),
@@ -25,4 +37,7 @@ export const songGetters = {
return song.tags.map((tag: any) => resolveTag(tag));
},
+ getStoreLinks: (song: any) => {
+ return song.storeLinks;
+ }
}
\ No newline at end of file