Working on a statistics page.

statistics_page
Sander Vocke 5 years ago
parent 6b89e618ce
commit bef55a6994
  1. 10
      client/src/components/MainWindow.tsx
  2. 8
      client/src/components/appbar/AddTabMenu.tsx
  3. 16
      client/src/components/common/StoreLinkIcon.tsx
  4. 13
      client/src/components/tables/ResultsTable.tsx
  5. 6
      client/src/components/windows/AlbumWindow.tsx
  6. 6
      client/src/components/windows/ArtistWindow.tsx
  7. 3
      client/src/components/windows/SongWindow.tsx
  8. 251
      client/src/components/windows/StatisticsWindow.tsx
  9. 6
      client/src/components/windows/TagWindow.tsx
  10. 11
      client/src/components/windows/Windows.tsx
  11. 9
      client/src/lib/MusicStore.tsx
  12. 17
      client/src/lib/songGetters.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 <StatisticsWindow
state={tabState}
dispatch={tabDispatch}
mainDispatch={dispatch}
/>
default:
throw new Error("Unimplemented window type");
}

@ -28,5 +28,13 @@ export default function AddTabMenu(props: IProps) {
})
}}
>{WindowType.Query}</MenuItem>
<MenuItem
onClick={() => {
props.onClose();
props.onCreateTab({
windowType: WindowType.Statistics,
})
}}
>{WindowType.Statistics}</MenuItem>
</Menu>
}

@ -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 <GPMIcon {...restProps}/>;
default:
throw new Error("Unknown external store: " + whichStore)

@ -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[],

@ -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;

@ -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;

@ -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;

@ -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<MusicStore, number>,
}
export interface AlbumStatistics {
numberOf: number,
noSongs: number,
noArtist: number,
perStore: Record<MusicStore, number>,
}
export interface ArtistStatistics {
numberOf: number,
noSongs: number,
noAlbums: number,
perStore: Record<MusicStore, number>,
}
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 <Box display="flex" alignItems="center">
<Box m={2}>
<Paper>
<Box p={2}>
<Box mb={1}><Typography variant="h4">Songs</Typography></Box>
<Typography>Total: {props.stats.songStats.numberOf}</Typography>
<Typography>Without album: {props.stats.songStats.noAlbum}</Typography>
<Typography>Without artist: {props.stats.songStats.noArtist}</Typography>
<Typography>Linked to Google Play Music: {props.stats.songStats.perStore[MusicStore.GooglePlayMusic]}</Typography>
</Box>
</Paper>
</Box>
<Box m={2}>
<Paper>
<Box p={2}>
<Box mb={1}><Typography variant="h4">Artists</Typography></Box>
<Typography>Total: {props.stats.artistStats.numberOf}</Typography>
<Typography>Without album: {props.stats.artistStats.noAlbums}</Typography>
<Typography>Without songs: {props.stats.artistStats.noSongs}</Typography>
<Typography>Linked to Google Play Music: {props.stats.artistStats.perStore[MusicStore.GooglePlayMusic]}</Typography>
</Box>
</Paper>
</Box>
<Box m={2}>
<Paper>
<Box p={2}>
<Box mb={1}><Typography variant="h4">Albums</Typography></Box>
<Typography>Total: {props.stats.albumStats.numberOf}</Typography>
<Typography>Without album: {props.stats.albumStats.noArtist}</Typography>
<Typography>Without songs: {props.stats.albumStats.noSongs}</Typography>
<Typography>Linked to Google Play Music: {props.stats.albumStats.perStore[MusicStore.GooglePlayMusic]}</Typography>
</Box>
</Paper>
</Box>
<Box m={2}>
<Paper>
<Box p={2}>
<Box mb={1}><Typography variant="h4">Tags</Typography></Box>
<Typography>Total: {props.stats.tagStats.numberOf}</Typography>
<Typography>Unused: {props.stats.tagStats.noItems}</Typography>
</Box>
</Paper>
</Box>
</Box >
}
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 <Box width="100%" justifyContent="center" display="flex" flexWrap="wrap">
<Box
m={1}
mt={4}
width="80%"
>
<EqualizerIcon style={{ fontSize: 80 }} />
</Box>
<Box
m={1}
mt={4}
width="80%"
>
<Box visibility={props.state.retrievingStats ? "visible" : "hidden"}><CircularProgress /></Box>
{props.state.stats && <StatisticsDisplay stats={props.state.stats} />}
</Box>
</Box>
}

@ -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 {

@ -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: <><EqualizerIcon/>Statistics</>,
stats: null,
retrievingStats: true,
}
}
}

@ -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;
}

@ -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;
}
}
Loading…
Cancel
Save