Initial support for song browsing from UI

This commit is contained in:
Deluan 2020-01-22 10:19:13 -05:00
parent fdf1ceeade
commit 9557f7ceed
17 changed files with 158 additions and 28 deletions

View file

@ -21,7 +21,7 @@ type Entry struct {
Starred time.Time
Track int
Duration int
Size string
Size int
Suffix string
BitRate int
ContentType string

View file

@ -18,7 +18,7 @@ type MediaFile struct {
TrackNumber int
DiscNumber int
Year int
Size string
Size int
Suffix string
Duration int
BitRate int

View file

@ -11,26 +11,26 @@ import (
)
type mediaFile struct {
ID string `orm:"pk;column(id)"`
Path string `orm:"index"`
Title string `orm:"index"`
Album string ``
Artist string ``
ArtistID string `orm:"column(artist_id)"`
AlbumArtist string ``
AlbumID string `orm:"column(album_id);index"`
HasCoverArt bool ``
TrackNumber int ``
DiscNumber int ``
Year int ``
Size string ``
Suffix string ``
Duration int ``
BitRate int ``
Genre string `orm:"index"`
Compilation bool ``
CreatedAt time.Time `orm:"null"`
UpdatedAt time.Time `orm:"null"`
ID string `json:"id" orm:"pk;column(id)"`
Path string `json:"path" orm:"index"`
Title string `json:"title" orm:"index"`
Album string `json:"album"`
Artist string `json:"artist"`
ArtistID string `json:"artistId" orm:"column(artist_id)"`
AlbumArtist string `json:"albumArtist"`
AlbumID string `json:"albumId" orm:"column(album_id);index"`
HasCoverArt bool `json:"-"`
TrackNumber int `json:"trackNumber"`
DiscNumber int `json:"discNumber"`
Year int `json:"year"`
Size int `json:"size"`
Suffix string `json:"suffix"`
Duration int `json:"duration"`
BitRate int `json:"bitRate"`
Genre string `json:"genre" orm:"index"`
Compilation bool `json:"compilation"`
CreatedAt time.Time `json:"createdAt" orm:"null"`
UpdatedAt time.Time `json:"updatedAt" orm:"null"`
}
type mediaFileRepository struct {

View file

@ -8,7 +8,6 @@ import (
"path"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
@ -243,7 +242,7 @@ func (s *TagScanner) toMediaFile(md *Metadata) model.MediaFile {
mf.BitRate = md.BitRate()
mf.Path = md.FilePath()
mf.Suffix = md.Suffix()
mf.Size = strconv.Itoa(md.Size())
mf.Size = md.Size()
mf.HasCoverArt = md.HasPicture()
// TODO Get Creation time. https://github.com/djherbis/times ?

View file

@ -53,6 +53,9 @@ func (app *Router) routes() http.Handler {
R(r, "/user", func(ctx context.Context) rest.Repository {
return app.ds.Resource(model.User{})
})
R(r, "/song", func(ctx context.Context) rest.Repository {
return app.ds.Resource(model.MediaFile{})
})
})
return r
}

View file

@ -181,7 +181,7 @@ func ToChild(entry engine.Entry) responses.Child {
child.CoverArt = entry.CoverArt
child.Track = entry.Track
child.Duration = entry.Duration
child.Size = entry.Size
child.Size = strconv.Itoa(entry.Size)
child.Suffix = entry.Suffix
child.BitRate = entry.BitRate
child.ContentType = entry.ContentType

View file

@ -2,6 +2,7 @@ package subsonic
import (
"net/http"
"strconv"
"github.com/cloudsonic/sonic-server/engine"
"github.com/cloudsonic/sonic-server/log"
@ -54,7 +55,7 @@ func (c *StreamController) Stream(w http.ResponseWriter, r *http.Request) (*resp
// contentLength = strconv.Itoa((mf.Duration + 1) * maxBitRate * 1000 / 8)
//}
h := w.Header()
h.Set("Content-Length", mf.Size)
h.Set("Content-Length", strconv.Itoa(mf.Size))
h.Set("Content-Type", "audio/mpeg")
h.Set("Expires", "0")
h.Set("Cache-Control", "must-revalidate")

View file

@ -5,6 +5,7 @@ import dataProvider from './dataProvider'
import authProvider from './authProvider'
import { Login } from './layout'
import user from './user'
import song from './song'
const App = () => (
<Admin
@ -12,6 +13,7 @@ const App = () => (
authProvider={authProvider}
loginPage={Login}
>
<Resource name="song" {...song} />
<Resource name="user" {...user} />
</Admin>
)

View file

@ -0,0 +1,18 @@
import React from 'react'
import PropTypes from 'prop-types'
const BitrateField = ({ record = {}, source }) => {
return <span>{`${record[source]} kbps`}</span>
}
BitrateField.propTypes = {
label: PropTypes.string,
record: PropTypes.object,
source: PropTypes.string.isRequired
}
BitrateField.defaultProps = {
addLabel: true
}
export default BitrateField

View file

@ -0,0 +1,25 @@
import React from 'react'
import PropTypes from 'prop-types'
const DurationField = ({ record = {}, source }) => {
return <span>{format(record[source])}</span>
}
const format = (d) => {
const date = new Date(null)
date.setSeconds(d)
const fmt = date.toISOString().substr(11, 8)
return fmt.replace(/^00:/, '')
}
DurationField.propTypes = {
label: PropTypes.string,
record: PropTypes.object,
source: PropTypes.string.isRequired
}
DurationField.defaultProps = {
addLabel: true
}
export default DurationField

7
ui/src/common/Title.js Normal file
View file

@ -0,0 +1,7 @@
import React from 'react'
const Title = ({ subTitle }) => {
return <span>CloudSonic {subTitle ? ` - ${subTitle}` : ''}</span>
}
export default Title

5
ui/src/common/index.js Normal file
View file

@ -0,0 +1,5 @@
import Title from './Title'
import DurationField from './DurationField'
import BitrateField from './BitrateField'
export { Title, DurationField, BitrateField }

59
ui/src/song/SongList.js Normal file
View file

@ -0,0 +1,59 @@
import React from 'react'
import {
BooleanField,
Datagrid,
DateField,
Filter,
List,
NumberField,
SearchInput,
Show,
SimpleShowLayout,
TextField
} from 'react-admin'
import { BitrateField, DurationField, Title } from '../common'
const SongFilter = (props) => (
<Filter {...props}>
<SearchInput source="title" alwaysOn />
</Filter>
)
const SongDetails = (props) => {
return (
<Show {...props} title=" ">
<SimpleShowLayout>
<TextField source="path" />
<TextField label="Album Artist" source="albumArtist" />
<TextField source="genre" />
<BooleanField source="compilation" />
<BitrateField source="bitRate" />
<DateField source="updatedAt" showTime />
</SimpleShowLayout>
</Show>
)
}
const SongList = (props) => (
<List
{...props}
title={<Title subTitle={'Songs'} />}
sort={{ field: 'title', order: 'ASC' }}
exporter={false}
bulkActionButtons={false}
filters={<SongFilter />}
perPage={15}
>
<Datagrid rowClick="expand" expand={<SongDetails />}>
<TextField source="title" />
<TextField source="album" />
<TextField source="artist" />
<NumberField label="Track #" source="trackNumber" />
<NumberField label="Disc #" source="discNumber" />
<TextField source="year" />
<DurationField label="Time" source="duration" />
</Datagrid>
</List>
)
export default SongList

7
ui/src/song/index.js Normal file
View file

@ -0,0 +1,7 @@
import MusicNote from '@material-ui/icons/MusicNote'
import SongList from './SongList'
export default {
list: SongList,
icon: MusicNote
}

View file

@ -8,9 +8,10 @@ import {
email,
SimpleForm
} from 'react-admin'
import { Title } from '../common'
const UserCreate = (props) => (
<Create {...props}>
<Create title={<Title subTitle={'Create User'} />} {...props}>
<SimpleForm redirect="list">
<TextInput source="userName" validate={[required()]} />
<TextInput source="name" validate={[required()]} />

View file

@ -9,9 +9,10 @@ import {
email,
SimpleForm
} from 'react-admin'
import { Title } from '../common'
const UserTitle = ({ record }) => {
return <span>User {record ? record.name : ''}</span>
return <Title subTitle={`User ${record ? record.name : ''}`} />
}
const UserEdit = (props) => (
<Edit title={<UserTitle />} {...props}>

View file

@ -10,6 +10,7 @@ import {
TextField
} from 'react-admin'
import { useMediaQuery } from '@material-ui/core'
import { Title } from '../common'
const UserFilter = (props) => (
<Filter {...props}>
@ -23,6 +24,7 @@ const UserList = (props) => {
return (
<List
{...props}
title={<Title subTitle={'Users'} />}
sort={{ field: 'userName', order: 'ASC' }}
exporter={false}
filters={<UserFilter />}