Merge branch 'develop'

This commit is contained in:
emeric
2018-09-12 13:59:18 +02:00
213 changed files with 10413 additions and 14346 deletions
+32 -9
View File
@@ -1,26 +1,49 @@
AUTOMAKE_OPTIONS = dist-bzip2 no-dist-gzip
SUBDIRS = test src
SUBDIRS = test tools src
lms_docrootdir=$(pkgdatadir)/docroot
lms_approotdir=$(pkgdatadir)/approot
lms_cssdir=$(lms_docrootdir)/css
lms_imagesdir=$(lms_docrootdir)/images
lms_jsdir=$(lms_docrootdir)/js
lms_css_DATA = \
docroot/css/lms.css
docroot/css/lms.css
lms_images_DATA = \
docroot/images/unknown-cover.jpg \
docroot/images/unknown-artist.jpg
docroot/images/unknown-cover.jpg \
docroot/images/unknown-artist.jpg
lms_js_DATA = \
docroot/js/bootstrap-notify.js \
docroot/js/jquery-1.10.2.min.js \
docroot/js/mediaplayer.js
lms_approot_DATA = \
approot/templates.xml
approot/admin-database.xml \
approot/admin-user.xml \
approot/admin-users.xml \
approot/admin-initwizard.xml \
approot/artist.xml \
approot/artists.xml \
approot/artistsinfo.xml \
approot/explore.xml \
approot/login.xml \
approot/mediaplayer.xml \
approot/messages.xml \
approot/playhistory.xml \
approot/playqueue.xml \
approot/release.xml \
approot/releases.xml \
approot/releasesinfo.xml \
approot/settings.xml \
approot/templates.xml \
approot/tracks.xml \
approot/tracksinfo.xml
install-data-hook:
ln -s $(datadir)/Wt/resources $(lms_docrootdir)/resources || echo "Skipped symlink creation"
sysconf_DATA = \
$(top_srcdir)/conf/lms.conf
uninstall-local:
cd $(lms_docrootdir) && rm -f -r $(lms_docrootdir)/resources
+67 -65
View File
@@ -1,93 +1,95 @@
# LMS - Lightweight Media Server
# LMS - Lightweight Music Server
LMS is a self-hosted media streaming software, released under the GPLv3 license.
It allows you to access your music and your videos using an https web interface.
It allows you to access your music using an http(s) web interface.
## Features
- Winamp-like interface, suited for large media collections
- Audio/Video transcode for maximum interoperability and low bandwith requirements
- User management
- Multi genre
- MusicBrainzID support
Please note LMS is still under development and will gain more features in the future:
- Radio function (using AcousticBrainz)
- Interface suited for mobile devices
- Video support
- Subsonic and/or Ampache API
## Main features
- Responsive design
- Browse your audio collection using tag-based filters
- Custom tag support
- User management
- Persistent play queue
- Radio mode
- MusicBrainzID support to handle duplicated artist and release names
- Audio transcode for maximum interoperability and low bandwith requirements
LMS is written entirely in C++. Therefore, it is suitable to run on embedded devices, where space and memory are limited.
Please note some media files may require significant CPU usage to be transcoded.
## Dependencies
### Debian or Ubuntu packages
## Installation
Here are the required packages to build on Debian Stretch:
```sh
$ apt-get install g++ autoconf automake libboost-dev libboost-locale-dev libboost-iostreams-dev libavcodec-dev libwtdbosqlite-dev libwthttp-dev libwtdbo-dev libwt-dev libmagick++-dev libavcodec-dev libavformat-dev libav-tools libpstreams-dev
apt-get install g++ autoconf automake libboost-filesystem-dev libboost-system-dev libavcodec-dev libavutil-dev libavformat-dev libav-tools libmagick++-dev libpstreams-dev libconfig++-dev libpstreams-dev ffmpeg libtag1-dev
```
## Build
You also need wt4, that is not packaged yet on Debian. See [installation instructions](https://www.webtoolkit.eu/wt/doc/reference/html/InstallationUnix.html).
```sh
$ autoreconf -vfi
$ mkdir build
$ cd build
$ ../configure --prefix=/usr
git clone https://github.com/epoupon/lms.git lms
cd lms
autoreconf -vfi
mkdir build
cd build
../configure --prefix=/usr --sysconfdir=/etc
```
configure will complain if a mandatory library is missing.
configure will report any missing library.
```sh
$ make -j 4
make
```
## Install
```sh
$ make install
make install
```
This command requires root privileges.
## Configuration
LMS uses a configuration file, installed in '/etc/lms.conf'. It is recommended to edit this file and change the relevant settings (working directory, listen port, etc.)
All other settings are set using the web interface.
It is highly recommended to run LMS as a non root user. Therefore make sure the user has write permissions on the working directory.
## Reverse proxy settings
You have to set the 'behind-reverse-proxy' option to 'true' in the configuration file.
Here is an example to make LMS properly work on myserver.org using nginx.
```
server {
listen 80;
server_name myserver.org;
access_log /var/log/nginx/myserver.access.log;
proxy_request_buffering off;
proxy_buffering off;
proxy_buffer_size 4k;
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://localhost:5091;
proxy_read_timeout 120;
}
}
```
This command requires root privileges
## Running
Here is a command line example to run on port 5081:
```sh
$ /usr/bin/lms --docroot='/usr/share/lms/docroot/;/resources,/css,/images,/favicon.ico' --approot=/usr/share/lms/approot --http-address 0.0.0.0 --http-port 5081
lms [config_file]
```
It is highly recommended to run LMS as a non root user.
The exectuable needs write accesses to the /var/lms/ directory.
Logs are output in the working directory, in the file 'lms.log'.
To connect to LMS, just open your favorite browser and go to http://localhost:5081
## Mobile
Add the following code in your wt_config.xml file:
```
<application-settings location="/usr/bin/lms">
<progressive-bootstrap>true</progressive-bootstrap>
</application-settings>
```
## Setting up SSL materials (optional)
Here is just a self signed certificate example, you could do use a CA if you want.
Generate a dh file:
```sh
$ openssl dhparam -out dh2048.pem 2048
```
Generate a self signed certificate:
```sh
$ openssl req -x509 -out cert.pem -keyout privkey.pem -newkey rsa:4096
```
Run (on port 5081):
```sh
$ /usr/bin/lms --docroot='/usr/share/lms/docroot/;/resources,/css,/images,/favicon.ico' --approot=/usr/share/lms/approot --https-address 0.0.0.0 --https-port 5081 --ssl-certificate cert.pem --ssl-private-key privkey.pem --ssl-tmp-dh dh2048.pem
```
Depending on your SSL parameters, you may be asked for the PEM passphrase to unlock the private key.
To connect to LMS, just open your favorite browser and go to https://localhost:5081
To connect to LMS, just open your favorite browser and go to http://localhost:5082
## Credits
- Wt (http://www.webtoolkit.eu/)
- bootstrap3 (http://getbootstrap.com/)
- libav project (https://www.libav.org/)
- ffmpeg project (https://ffmpeg.org/)
- Magick++ (http://www.imagemagick.org/Magick++/)
- MetaBrainz (https://metabrainz.org/)
- Bootstrap Notify: https://github.com/mouse0270/bootstrap-notify
+14 -51
View File
@@ -1,73 +1,36 @@
[Book]
- Make the feature?
[Build]
- Makefile per directory. Use the include flags only where needed?
[Cover]
- Handle preferred cover file names ("front.xxx", "cover.xxx", ...)
- Implement a cache and a grabber from some web service (mandatory for artists?)
- Implement a cache to improve speed
[Database]
- Optim, use SQL query to get the "genre" orphans
- Use Inotify like system to watch modified/added files?
- handle access rights problems (instead of aborting)
- add a global play counter for tracks. This will help people to spot most popular files
- rework the exception process in av/metadata/updater in case of bad files
- Use the WServer::post method to notify the end of the database scan? (with results?)
- Use albumartist when available (useful for compilations)
- Add AcousticBrainz support to create new kind of clusters
[Metadata]
- OGG metadata -> properly handle metadata nested in the audio stream
- WMA covers: add support
[Playlist]
- Save/Load playlists
- Make public playlists so that users can see what other people are listening to
[Users]
- Handle login lifetime?
- Use the WServer::post method to kick users already logged in?
- Limit multi login from UI and API interfaces (limit per interface is acceptable)
[Services]
- consider adding support for external web servers (new FCGI service?)
[UI]
- Handle internationalization
- Handle internal path
- Cannot log again when refreshing the application in firefox (have to close/open a new browser tab)
[Settings]
- logout users that are being changed (loss of admin rights), or make sure they are still admin when they make changes
- "signal not exposed" problem if a user logout and login again. Bad resource destruction?
- add a scroll area in order for settings to work on mobile devices
[user/transcoding]
- Prefered codecs for video
- use a slider instead of a combo box for the bitrates?
[admin/Users]
- do not activate the update period/start time when 'admin' is checked
- do not activate the bitrate limit when creating a new admin user
[admin/Database update]
- do not make the update start time field active when update perdiod is "Never"
- refresh the audio/video view when the database directories have been updated
- Add a download button to get the current playqueue in a streamed zip file
- Save user prefs in local storage (volume, loop, radio mode, etc.)
- Add global keyboard shortcuts
- scrollbar in player
- display a play history somewhere
[Audio]
- drop the winamp like interface?
- Add a download button to get the current playlist in a streamed zip file
- Add a upload button to upload media files in a dedicated directory
- Perform a search for each key pressed (using a timer, in JS)
- Save user prefs in local storage (volume, loop, shuffle, current playqueue, etc.)
[desktop]
- TrackView: Reselect the current selected item when displaying the updated search results
- Add keyboard shortcuts
- playqueue: try to get a single div to display the cover / artist / track name in order to improve style
[mobile]
- Reorganize the views to ease internal paths
- Covers not always the same height
- 'duration' and 'play' tags are broken if title is too long
- Cover loading breaks the vertical alignment
- Implement a play queue
- Implement a decent player
- Make the release/artist clickable when displayed to the user
[Video]
- implement a decent mediaplayer
- remove codec choice from parameters
[Settings]
- Add multi root directories support
[REST API]
- Make a dedicated REST API. Maybe use the SubSonic API or Ampache API?
+68
View File
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<!--FORMS message blocks-->
<message id="Lms.Admin.Database.template">
<legend>${tr:Lms.Admin.Database.database}</legend>
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-sm-2" for="${id:media-directory}">
${tr:Lms.Admin.Database.path}
</label>
<div class="col-sm-5">
${media-directory}
</div>
<div class="help-block col-sm-5">
${media-directory-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:update-period}">
${tr:Lms.Admin.Database.update-period}
</label>
<div class="col-sm-5">
${update-period}
</div>
<div class="help-block col-sm-5">
${update-period-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:update-start-time}">
${tr:Lms.Admin.Database.update-start-time}
</label>
<div class="col-sm-5">
${update-start-time}
</div>
<div class="help-block col-sm-5">
${update-start-time-info}
</div>
</div>
</div>
<legend>${tr:Lms.Admin.Database.scan-options}</legend>
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-sm-2" for="${id:tags}">
${tr:Lms.Admin.Database.tags}
</label>
<div class="col-sm-5">
${tags}
</div>
<div class="help-block col-sm-5">
${tags-info}
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
${apply-btn class="btn-primary"} ${discard-btn} ${immediate-scan-btn class="btn-info"}
</div>
</div>
</div>
</message>
</messages>
+56
View File
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<!--FORMS message blocks-->
<message id="Lms.Admin.InitWizard.template">
<div class="container">
<div class="page-header">
<h2>${tr:Lms.Admin.InitWizard.header}</h2>
</div>
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-sm-2" for="${id:admin-login}">
${tr:Lms.login}
</label>
<div class="col-sm-5">
${admin-login}
</div>
<div class="help-block col-sm-5">
${admin-login-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:password}">
${tr:Lms.password}
</label>
<div class="col-sm-5">
${password}
</div>
<div class="help-block col-sm-5">
${password-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:password-confirm}">
${tr:Lms.password-confirm}
</label>
<div class="col-sm-5">
${password-confirm}
</div>
<div class="help-block col-sm-5">
${password-confirm-info}
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
${create-btn class="btn-primary"}
</div>
</div>
</div>
</div>
</message>
</messages>
+69
View File
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<message id="Lms.Admin.User.template">
<div class="page-header">
<h2>${title}</h2>
</div>
<div class="form-horizontal">
${<if-has-login>}
<div class="form-group">
<label class="control-label col-sm-2" for="${id:login}">
${tr:Lms.login}
</label>
<div class="col-sm-5">
${login}
</div>
<div class="help-block col-sm-5">
${login-info}
</div>
</div>
${</if-has-login>}
<div class="form-group">
<label class="control-label col-sm-2" for="${id:password}">
${tr:Lms.password}
</label>
<div class="col-sm-5">
${password}
</div>
<div class="help-block col-sm-5">
${password-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:audio-bitrate-limit}">
${tr:Lms.Admin.User.audio-bitrate-limit}
</label>
<div class="col-sm-5">
<div class="input-group">
${audio-bitrate-limit}
<span class="input-group-addon">kbps</span>
</div>
</div>
<div class="help-block col-sm-5">
${audio-bitrate-limit-info}
</div>
</div>
${<if-demo>}
<div class="form-group">
<label class="control-label col-sm-2" for="${id:demo}">
${tr:Lms.Admin.User.demo-account}
</label>
<div class="col-sm-5">
${demo}
</div>
<div class="help-block col-sm-5">
${demo-info}
</div>
</div>
${</if-demo>}
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
${save-btn class="btn-primary"}
</div>
</div>
</div>
</message>
</messages>
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<message id="Lms.Admin.Users.template">
<div class="page-header">
<h2>${tr:Lms.Admin.Users.users}</h2>
</div>
${users}
${add-btn class="btn-primary Lms-admin-users-add-btn"}
</message>
<message id="Lms.Admin.Users.template.entry">
<div class="row Lms-admin-users-entry Lms-vertical-align">
<div class="col-xs-7 Lms-vertical-align">
${name}${<if-tag>}<span class="badge Lms-admin-users-entry-role">${tag}</span>${</if-tag>}
</div>
<div class="col-xs-5 Lms-horizontal-center">
${<if-edit>}${edit-btn class="Lms-admin-users-entry-btn"}${del-btn class="btn-danger Lms-admin-users-entry-btn"}${</if-edit>}
</div>
</div>
</message>
</messages>
+34
View File
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<!--FORMS message blocks-->
<message id="Lms.Explore.Artist.template">
<div class="page-header">
<h2>${name}</h2>
${clusters}
${play-btn}${add-btn}
</div>
${releases}
</message>
<message id="Lms.Explore.Artist.template.entry">
<div class="row">
<div class="col-xs-12 Lms-explore-artist-entry">
<div class="media">
<div class="media-left media-middle">
${cover class="media-object"}
</div>
<div class="media-body media-middle">
<h4 class="media-heading">
${name}
</h4>
${<if-has-various-artists>}<h4><small><i>${tr:Lms.Explore.various-artists}</i></small></h4>${</if-has-various-artists>}
<h4><small>${<if-has-year>}${year}${</if-has-year>}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>}</small></h4>
${play-btn}${add-btn}
</div>
</div>
</div>
</div>
</message>
</messages>
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<!--FORMS message blocks-->
<message id="Lms.Explore.Artists.template">
<div class="page-header">
<h2>${tr:Lms.Explore.artists}</h2>
</div>
<div class="form-group">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-search"></i></span>
${search}
</div>
</div>
${artists}
<div class="row">
<div class="col-xs-12 Lms-horizontal-center">
${show-more class="btn-primary Lms-show-more"}
</div>
</div>
</message>
<message id="Lms.Explore.Artists.template.entry">
<div class="row">
<div class="col-xs-12 Lms-explore-artists-entry Lms-vertical-align">
<h4>${name}</h4>
</div>
</div>
</message>
</messages>
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<!--FORMS message blocks-->
<message id="Lms.Explore.ArtistsInfo.template">
<h3>${tr:Lms.Explore.ArtistsInfo.most-played}</h3>
${most-played}
<h3>${tr:Lms.Explore.ArtistsInfo.recently-added}</h3>
${recently-added}
</message>
<message id="Lms.Explore.ArtistsInfo.template.entry">
<div class="row Lms-explore-artistsinfo-entry Lms-vertical-align">
<div class="col-xs-12">
${name}
</div>
</div>
</message>
</messages>
+59
View File
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<message id="Lms.Explore.template">
<div class="row">
<div class="col-lg-4 col-lg-push-8">
${filters class="Lms-explore-filters"}
${info class="Lms-explore-info hidden-md hidden-xs"}
</div>
<div class="col-lg-8 col-lg-pull-4">
${contents}
</div>
</div>
</message>
<message id="Lms.Explore.template.add-btn">
<i class="fa fa-plus-circle Lms-explore-btn"></i>
</message>
<message id="Lms.Explore.template.play-btn">
<i class="fa fa-play-circle Lms-explore-btn"></i>
</message>
<message id="Lms.Explore.template.filters">
<h3>${tr:Lms.Explore.filters} ${add-filter class="btn-primary"}</h3>
${clusters}
</message>
<message id="Lms.Explore.template.cluster-entry">
<span class="badge {1}">${<if-can-delete>}<i class="fa fa-times-circle"></i> ${</if-can-delete>}${name}</span>
</message>
<message id="Lms.Explore.template.add-filter">
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-sm-2" for="${id:type}">
${tr:Lms.Explore.type}
</label>
<div class="col-sm-10">
${type}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:value}">
${tr:Lms.Explore.value}
</label>
<div class="col-sm-10">
${value}
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
${add-btn class="btn-primary"} ${cancel-btn}
</div>
</div>
</div>
</message>
</messages>
+56
View File
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<message id="Lms.Auth.template">
<div class="container">
<div class="page-header">
<h2>${tr:Lms.Auth.welcome}</h2>
</div>
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-sm-2" for="${id:user-name}">
${tr:Lms.login}
</label>
<div class="col-sm-5">
${user-name}
</div>
<div class="help-block col-sm-5">
${user-name-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:password}">
${tr:Lms.password}
</label>
<div class="col-sm-5">
${password}
</div>
<div class="help-block col-sm-5">
${password-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:remember-me}">
${tr:Lms.Auth.remember-me}
</label>
<div class="col-sm-5">
${remember-me}
</div>
<div class="help-block col-sm-5">
${remember-me-info}
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
${login-btn class="btn-primary"}
</div>
</div>
</div>
</div>
</message>
</messages>
+36
View File
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<!--FORMS message blocks-->
<message id="Lms.MediaPlayer.template">
<audio id="lms-mp-audio"></audio>
<div class="container">
<div class="Lms-player-seek-container">
<input id="lms-mp-seek" class="Lms-player-seek-common Lms-player-seek" type="range" min="0" max="100" step="1" value="0"/>
<div class="progress Lms-player-seek-common Lms-player-progress">
<div id="lms-mp-progress" class="progress-bar progress-bar-info progress-bar-striped active" role="progressbar" aria-valuenow="20" aria-valuemin="0" aria-valuemax="100" style="width: 0%">
</div>
</div>
</div>
<div class="row">
<div class="col-xs-4 Lms-player-controls">
<i id="lms-mp-previous" class="fa fa-step-backward Lms-player-btn"></i>
<i id="lms-mp-play" class="fa fa-play Lms-player-btn Lms-player-btn-playpause"></i>
<i id="lms-mp-pause" class="fa fa-pause Lms-player-btn Lms-player-btn-playpause" style="display: none"></i>
<i id="lms-mp-next" class="fa fa-step-forward Lms-player-btn"></i>
</div>
<div class="col-xs-8">
<div class="Lms-player-text-center">
${title class="Lms-player-text"}
</div>
<div class="Lms-player-text-center">
<small><span class="hidden-xs">${release class="Lms-player-text"}</span><i>${artist class="Lms-player-text"}</i></small>
</div>
</div>
</div>
</div>
</message>
</messages>
+115
View File
@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<!--Common-->
<message id="Lms.add">Add</message>
<message id="Lms.apply">Apply</message>
<message id="Lms.discard">Discard</message>
<message id="Lms.administration">Administration</message>
<message id="Lms.cancel">Cancel</message>
<message id="Lms.create">Create</message>
<message id="Lms.login">Login</message>
<message id="Lms.logout">Logout</message>
<message id="Lms.not-a-directory">Not a directory</message>
<message id="Lms.password">Password</message>
<message id="Lms.password-confirm">Confirm password</message>
<message id="Lms.passwords-dont-match">Passwords don't match</message>
<message id="Lms.quit-confirm">Are you sure?</message>
<message id="Lms.quit-other-session">Another session has been open. Reopen this one?</message>
<message id="Lms.save">Save</message>
<!--Administration-->
<message id="Lms.Admin.Database.daily">Daily</message>
<message id="Lms.Admin.Database.database">Database</message>
<message id="Lms.Admin.Database.immediate-scan">Scan now!</message>
<message id="Lms.Admin.Database.monthly">Monthly</message>
<message id="Lms.Admin.Database.never">Never</message>
<message id="Lms.Admin.Database.path">Media root directory</message>
<message id="Lms.Admin.Database.scan-complete">Scan complete: {1} total files, {2} additions, {3} updates, {4} deletions, {5} duplicates, {6} errors</message>
<message id="Lms.Admin.Database.scan-launched">Scan launched!</message>
<message id="Lms.Admin.Database.scan-options">Scan options</message>
<message id="Lms.Admin.Database.settings-saved">New settings saved!</message>
<message id="Lms.Admin.Database.tags">Tags</message>
<message id="Lms.Admin.Database.update-period">Update period</message>
<message id="Lms.Admin.Database.update-start-time">Update start time</message>
<message id="Lms.Admin.Database.weekly">Weekly</message>
<!--Users-->
<message id="Lms.Admin.Users.add">New user</message>
<message id="Lms.Admin.Users.admin">Admin</message>
<message id="Lms.Admin.Users.del">Delete</message>
<message id="Lms.Admin.Users.del-user">Delete user</message>
<message id="Lms.Admin.Users.del-user-name">Delete user?</message>
<message id="Lms.Admin.Users.demo">Demo</message>
<message id="Lms.Admin.Users.edit">Edit</message>
<message id="Lms.Admin.Users.users">Users</message>
<!--User-->
<message id="Lms.Admin.User.audio-bitrate-limit">Audio bitrate limit</message>
<message id="Lms.Admin.User.demo-account">Demo account</message>
<message id="Lms.Admin.User.demo-account-already-exists">Demo account already exists!</message>
<message id="Lms.Admin.User.demo-password-invalid">Demo password must be the login name!</message>
<message id="Lms.Admin.User.user-already-exists">User already exists!</message>
<message id="Lms.Admin.User.user-create">New user</message>
<message id="Lms.Admin.User.user-created">New user created!</message>
<message id="Lms.Admin.User.user-edit">Edit user {1}</message>
<message id="Lms.Admin.User.user-updated">User updated!</message>
<!--Wizard-->
<message id="Lms.Admin.InitWizard.done">Administration account created. Please refresh to continue!</message>
<message id="Lms.Admin.InitWizard.header">Create administrator account</message>
<!--Auth-->
<message id="Lms.Auth.remember-me">Remember me</message>
<message id="Lms.Auth.welcome">Welcome!</message>
<!--Explore-->
<message id="Lms.Explore.add-filter">Add filter</message>
<message id="Lms.Explore.artists">Artists</message>
<message id="Lms.Explore.filters">Filters</message>
<message id="Lms.Explore.releases">Albums</message>
<message id="Lms.Explore.search-placeholder">Search...</message>
<message id="Lms.Explore.show-more">Show more</message>
<message id="Lms.Explore.tracks">Tracks</message>
<message id="Lms.Explore.type">Type</message>
<message id="Lms.Explore.value">Value</message>
<message id="Lms.Explore.various-artists">Various artists</message>
<!--Explore:Artists-->
<message id="Lms.Explore.ArtistsInfo.recently-added">Recently added</message>
<message id="Lms.Explore.ArtistsInfo.most-played">Top artists</message>
<!--Explore:Releases-->
<message id="Lms.Explore.ReleasesInfo.recently-added">Recently added</message>
<message id="Lms.Explore.ReleasesInfo.most-played">Top releases</message>
<!--Explore:Tracks-->
<message id="Lms.Explore.TracksInfo.recently-added">Recently added</message>
<message id="Lms.Explore.TracksInfo.most-played">Top tracks</message>
<!--Playque-->
<message id="Lms.PlayQueue.nb-tracks">{1} tracks</message>
<message id="Lms.PlayQueue.nb-tracks-added">Added {1} tracks</message>
<message id="Lms.PlayQueue.nb-tracks-playing">Playing {1} tracks</message>
<message id="Lms.PlayQueue.playqueue">Play Queue</message>
<message id="Lms.PlayQueue.radio-mode">Radio mode</message>
<!--PlayHistory-->
<message id="Lms.PlayHistory.playhistory">Play History</message>
<!--Settings-->
<message id="Lms.Settings.account">Account</message>
<message id="Lms.Settings.audio">Audio</message>
<message id="Lms.Settings.auto">Auto</message>
<message id="Lms.Settings.bitrate">Bitrate</message>
<message id="Lms.Settings.demo-cannot-save">Cannot save using a demo account!</message>
<message id="Lms.Settings.encoding">Encoding</message>
<message id="Lms.Settings.mp3">MP3</message>
<message id="Lms.Settings.oga">OGA</message>
<message id="Lms.Settings.settings">Settings</message>
<message id="Lms.Settings.webma">WEBMA</message>
<message id="Lms.Settings.settings-saved">New settings saved!</message>
</messages>
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<!--FORMS message blocks-->
<message id="Lms.PlayHistory.template">
<div class="page-header">
<h2>${tr:Lms.PlayHistory.playhistory}</h2>
</div>
${entries}
<div class="row">
<div class="col-xs-12 Lms-horizontal-center">
${show-more class="btn-primary Lms-show-more"}
</div>
</div>
</message>
<message id="Lms.PlayHistory.template.entry">
<div class="row Lms-playhistory-entry Lms-vertical-align">
<div class="col-xs-9">
<h4>${name}</h4>
</div>
<div class="col-sm-3 hidden-xs">
${<if-has-release>}<small>${release-name}</small>${</if-has-release>}
</div>
<div class="col-sm-3 hidden-xs">
${<if-has-artist>}<small><i>${artist-name}</i></small>${</if-has-artist>}
</div>
</div>
</message>
</messages>
+54
View File
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<!--FORMS message blocks-->
<message id="Lms.PlayQueue.clear">
<i class="fa fa-times-circle Lms-playqueue-btn"></i>
</message>
<message id="Lms.PlayQueue.shuffle">
<i class="fa fa-random Lms-playqueue-btn"></i>
</message>
<message id="Lms.PlayQueue.template">
<div class="page-header">
<h2>${tr:Lms.PlayQueue.playqueue}</h2>
${clear-btn}
${shuffle-btn}
${radio-mode}
<h4><small>${nb-tracks}</small></h4>
</div>
${entries}
<div class="row">
<div class="col-xs-12 Lms-horizontal-center">
${show-more class="btn-primary Lms-show-more"}
</div>
</div>
</message>
<message id="Lms.PlayQueue.play">
<i class="fa fa-play-circle Lms-playqueue-btn"></i>
</message>
<message id="Lms.PlayQueue.delete">
<i class="fa fa-times-circle Lms-playqueue-btn"></i>
</message>
<message id="Lms.PlayQueue.template.entry">
<div class="row Lms-playqueue-entry Lms-vertical-align">
<div class="col-xs-9">
<h4>${name}</h4>
</div>
<div class="col-sm-3 hidden-xs">
${<if-has-release>}<small>${release-name}</small>${</if-has-release>}
</div>
<div class="col-sm-3 hidden-xs">
${<if-has-artist>}<small><i>${artist-name}</i></small>${</if-has-artist>}
</div>
<div class="col-xs-3 Lms-playqueue-entry-controls">
${play-btn}${del-btn}
</div>
</div>
</message>
</messages>
+36
View File
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<!--FORMS message blocks-->
<message id="Lms.Explore.Release.template">
<div class="row">
<div class="col-md-12">
${cover class="Lms-explore-release-cover"}
</div>
</div>
<div class="page-header">
<h3>${name}</h3>
${<if-has-artist>}<h4><i>${artist-name}</i></h4>${</if-has-artist>}
<h4><small>${<if-has-year>}${year}${</if-has-year>}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>}</small></h4>
${clusters}
${play-btn}${add-btn}
</div>
${tracks}
</message>
<message id="Lms.Explore.Release.template.entry">
<div class="row Lms-explore-release-entry Lms-vertical-align">
<div class="col-xs-9 Lms-explore-release-entry-name">
${<if-has-disc-number>}${disc-number}-${</if-has-disc-number>}${<if-has-track-number>}${track-number}. ${</if-has-track-number>}
${<if-has-artist>}<i>${artist-name}</i> - ${</if-has-artist>}
${name}
</div>
<div class="col-xs-3 Lms-explore-release-entry-controls">
${play-btn}${add-btn}
</div>
</div>
</message>
</messages>
+42
View File
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<message id="Lms.Explore.Releases.template">
<div class="page-header">
<h2>${tr:Lms.Explore.releases}</h2>
</div>
<div class="form-group">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-search"></i></span>
${search}
</div>
</div>
${releases}
<div class="row">
<div class="col-xs-12 Lms-horizontal-center">
${show-more class="btn-primary Lms-show-more"}
</div>
</div>
</message>
<message id="Lms.Explore.Releases.template.entry">
<div class="row">
<div class="col-xs-12 Lms-explore-releases-entry">
<div class="media">
<div class="media-left media-middle">
${cover class="media-object"}
</div>
<div class="media-body media-middle">
<h4 class="media-heading">
${release-name}
</h4>
${<if-has-artist>}<h4><small><i>${artist-name}</i></small></h4>${</if-has-artist>}
${play-btn}${add-btn}
</div>
</div>
</div>
</div>
</message>
</messages>
+30
View File
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<message id="Lms.Explore.ReleasesInfo.template">
<h3>${tr:Lms.Explore.ReleasesInfo.most-played}</h3>
${most-played}
<h3>${tr:Lms.Explore.ReleasesInfo.recently-added}</h3>
${recently-added}
</message>
<message id="Lms.Explore.ReleasesInfo.template.entry">
<div class="row Lms-explore-releasesinfo-entry">
<div class="col-xs-12">
<div class="media">
<div class="media-left media-middle">
${cover class="media-object"}
</div>
<div class="media-body media-middle">
<div class="media-heading">
${release-name}
</div>
${<if-has-artist>}<small><i>${artist-name}</i></small>${</if-has-artist>}
</div>
</div>
</div>
</div>
</message>
</messages>
+68
View File
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<!--FORMS message blocks-->
<message id="Lms.Settings.template">
<legend>${tr:Lms.Settings.audio}</legend>
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-sm-2" for="${id:bitrate}">
${tr:Lms.Settings.bitrate}
</label>
<div class="col-sm-5">
<div class="input-group">
${bitrate}
<span class="input-group-addon">kbps</span>
</div>
</div>
<div class="help-block col-sm-5">
${bitrate-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:encoding}">
${tr:Lms.Settings.encoding}
</label>
<div class="col-sm-5">
${encoding}
</div>
<div class="help-block col-sm-5">
${encoding-info}
</div>
</div>
</div>
<legend>${tr:Lms.Settings.account}</legend>
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-sm-2" for="${id:password}">
${tr:Lms.password}
</label>
<div class="col-sm-5">
${password}
</div>
<div class="help-block col-sm-5">
${password-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:password-confirm}">
${tr:Lms.password-confirm}
</label>
<div class="col-sm-5">
${password-confirm}
</div>
<div class="help-block col-sm-5">
${password-confirm-info}
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
${apply-btn class="btn-primary"} ${discard-btn}
</div>
</div>
</div>
</message>
</messages>
+4 -522
View File
@@ -1,530 +1,12 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<!--FORMS message blocks-->
<message id="Wt.WNavigationBar.template">
<div>
${collapse-button}
<div class="navbar-header">
${expand-button}
${title-link}
${search}
</div>
${contents}
</div>
</message>
<message id="firstConnectionForm-template">
<legend>${title}</legend>
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-sm-2" for="${id:name}">
Name
</label>
<div class="col-sm-5">
${name}
</div>
<div class="help-block col-sm-5">
${name-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:email}">
e-Mail
</label>
<div class="col-sm-5">
${email}
</div>
<div class="help-block col-sm-5">
${email-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:password}">
Password
</label>
<div class="col-sm-5">
${password}
</div>
${password-info}
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:password-confirm}">
Confirm password
</label>
<div class="col-sm-5">
${password-confirm}
</div>
<div class="help-block col-sm-5">
${password-confirm-info}
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
${save-button}
</div>
</div>
${apply-info}
</div>
</message>
<message id="userForm-template">
<legend>${title}</legend>
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-sm-2" for="${id:name}">
Name
</label>
<div class="col-sm-5">
${name}
</div>
<div class="help-block col-sm-5">
${name-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:email}">
e-Mail
</label>
<div class="col-sm-5">
${email}
</div>
<div class="help-block col-sm-5">
${email-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:password}">
Password
</label>
<div class="col-sm-5">
${password}
</div>
<div class="help-block col-sm-5">
${password-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:password-confirm}">
Confirm password
</label>
<div class="col-sm-5">
${password-confirm}
</div>
<div class="help-block col-sm-5">
${password-confirm-info}
</div>
</div>
</div>
<legend>${access}</legend>
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-sm-2" for="${id:admin}">
Admin
</label>
<div class="col-sm-5">
${admin}
</div>
<div class="help-block col-sm-5">
${admin-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:audio-bitrate-limit}">
Audio Bitrate Limit
</label>
<div class="col-sm-5">
<div class="input-group">
${audio-bitrate-limit}
<span class="input-group-addon">kbps</span>
</div>
</div>
<div class="help-block col-sm-5">
${audio-bitrate-limit-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:video-bitrate-limit}">
Video Bitrate Limit
</label>
<div class="col-sm-5">
<div class="input-group">
${video-bitrate-limit}
<span class="input-group-addon">kbps</span>
</div>
</div>
<div class="help-block col-sm-5">
${video-bitrate-limit-info}
</div>
</div>
</div>
<div class="form-horizontal">
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
${save-button} ${cancel-button}
</div>
</div>
</div>
</message>
<message id="userAccountForm-template">
<legend>${title}</legend>
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-sm-2" for="${id:name}">
Name
</label>
<div class="col-sm-5">
${name}
</div>
<div class="help-block col-sm-5">
${name-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:email}">
e-Mail
</label>
<div class="col-sm-5">
${email}
</div>
<div class="help-block col-sm-5">
${email-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:password}">
Password
</label>
<div class="col-sm-5">
${password}
</div>
<div class="help-block col-sm-5">
${password-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:password-confirm}">
Confirm password
</label>
<div class="col-sm-5">
${password-confirm}
</div>
<div class="help-block col-sm-5">
${password-confirm-info}
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
${save-button} ${cancel-button}
</div>
</div>
${apply-info}
</div>
</message>
<message id="audioForm-template">
<legend>${title}</legend>
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-sm-2" for="${id:bitrate}">
Audio bitrate
</label>
<div class="col-sm-5">
<div class="input-group">
${bitrate}
<span class="input-group-addon">kbps</span>
</div>
</div>
<div class="help-block col-sm-5">
${bitrate-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:encoding}">
Audio Encoding
</label>
<div class="col-sm-5">
${encoding}
</div>
<div class="help-block col-sm-5">
${encoding-info}
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
${save-button} ${cancel-button}
</div>
</div>
${apply-info}
</div>
</message>
<message id="mediaDirectoryForm-template">
<legend>${title}</legend>
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-sm-2" for="${id:path}">
Path
</label>
<div class="col-sm-5">
${path}
</div>
<div class="help-block col-sm-5">
${path-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:type}">
Type
</label>
<div class="col-sm-5">
${type}
</div>
<div class="help-block col-sm-5">
${type-info}
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
${save-button} ${cancel-button}
</div>
</div>
${apply-info}
</div>
</message>
<message id="databaseForm-template">
<legend>${title}</legend>
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-sm-2" for="${id:update-period}">
Update period
</label>
<div class="col-sm-5">
${update-period}
</div>
<div class="help-block col-sm-5">
${update-period-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:update-start-time}">
Update start time
</label>
<div class="col-sm-5">
${update-start-time}
</div>
<div class="help-block col-sm-5">
${update-start-time-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:audio-file-extensions}">
Audio file extensions
</label>
<div class="col-sm-5">
${audio-file-extensions}
</div>
<div class="help-block col-sm-5">
${audio-file-extensions-info}
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="${id:video-file-extensions}">
Video file extensions
</label>
<div class="col-sm-5">
${video-file-extensions}
</div>
<div class="help-block col-sm-5">
${video-file-extensions-info}
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
${apply-button} ${discard-button} ${immediate-scan-button}
</div>
</div>
${apply-info}
</div>
</message>
<message id="mobile-search-title">
<div class="row">
<div class="col-xs-12">
<div class="mobile-search-title">${text}</div>
</div>
</div>
</message>
<message id="mobile-search-more">
<div class="row">
<div class="col-xs-12">
<div class="mobile-search-more vertical-align">${text}</div>
</div>
</div>
</message>
<message id="mobile-audio-footer">
<nav class="navbar navbar-default navbar-fixed-bottom" role="navigation">
${player}
</nav>
</message>
<!--
wa-artist-search
-> wa-artist-search-res
-> wa-artist-search-res
-> ...
-->
<message id="wa-artist-search">
${title}
${contents}
${show-more}
</message>
<message id="wa-artist-search-res">
<div class="col-xs-6 col-md-2">
<br/>${gif}
<h4 class="release_truncated-name" title="${artistname}">${name}<div style="font-size: 75%"></div></h4>
</div>
</message>
<!--
wa-release-search
-> wa-release-search-res
-> wa-release-search-res
-> ...
-->
<message id="wa-release-search">
${title}
${contents}
${show-more}
</message>
<message id="wa-release-search-res">
<div class="col-xs-6 col-md-2">
<br/>${cover}
<h4 class="release_truncated-name" title="${release_name}">${name}<div style="font-size: 75%"><br/>${artist}<br/></div></h4>
</div>
</message>
<!--
wa-track-search
-> wa-track-search-res
-> wa-track-search-res
-> ...
!-->
<message id="wa-track-search">
${title}
<div class="menu-category list-group ">
${contents}
</div>
${show-more}
</message>
<message id="wa-track-search-res">
<div class="menu-item list-group-item ">
<div class="row">
<div class="vertical-align">
<div class="col-xs-2" style="margin-top:3px">${cover}</div>
<div class="col-xs-8">${artist-name}<br/>${track-name}</div>
<div class="col-xs-2">${play-btn}${add-btn}</div>
</div>
</div>
</div>
</message>
<!-- Release view
Track bar title
Release1
Track1
Track2
...
Release2
...
...
-->
<message id="wa-trackview-wrapper">
${title}
${release-container}
${show-more}
</message>
<message id="wa-trackview-release-container">
<message id="Lms.template">
${navbar-top class="main-nav"}
<div class="container">
<div class="col-md-3">
<br/>
<br/>
${cover}
<br />
<h4>${release-name} <br/>${artist-name}${<if-has-year>} - ${year}${</if-has-year>}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>}</h4>
</div>
<div class="menu row col-md-9">
<br/>
<div class="menu-category list-group ">
${track-container}
</div>
</div>
${contents class="Lms-contents"}
</div>
</message>
<message id="wa-trackview-track">
<div class="menu-item list-group-item ">${<if-has-disc-num>}${disc-num}-${</if-has-disc-num>}${<if-has-track-num>}${track-num}. ${</if-has-track-num>}${track-name} ${play-btn}${add-btn}</div>
</message>
<!-- Playqueue view
!-->
<message id="wa-playqueue-view">
${controls}
<div class="menu-category list-group">
${contents}
</div>
</message>
<message id="wa-playqueue-controls">
${load}${save}${clear}${shuffle}${repeat}
</message>
<message id="wa-playqueue-track">
<div class="row">
<div class="vertical-align">
<div class="col-xs-2" style="margin-top:3px">${cover}</div>
<div class="col-xs-8">${artist-name}<br/>${track-name}</div>
<div class="col-xs-2" style="margin-top:3px">${play-btn}${del-btn}</div>
</div>
</div>
</message>
<message id="wa-audio-player">
${audio}
${prev}${play-pause}${next}
<div class="mediaplayer-volume-container hidden-xs">
${volume}
</div>
<div class="mediaplayer-cover hidden-xs">
${cover}
</div>
<div class="mediaplayer-info-container">
<div class="mediaplayer-track-info">
<span class="mediaplayer-track">${track}</span> <span class="mediaplayer-artist">${artist}</span>
</div>
<div class="mediaplayer-progress-container">
${curtime}${seekbar}${duration}
</div>
</div>
${<if-has-shuffle>}${shuffle}${</if-has-shuffle>}${<if-has-repeat>}${repeat}${</if-has-repeat>}${<if-has-playqueue>}${playqueue}${</if-has-playqueue>}
${player class="Lms-player"}
</message>
</messages>
+42
View File
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<!--FORMS message blocks-->
<message id="Lms.Explore.Tracks.template">
<div class="page-header">
<h2>${tr:Lms.Explore.tracks}</h2>
${play-btn}${add-btn}
</div>
<div class="form-group">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-search"></i></span>
${search}
</div>
</div>
${tracks}
<div class="row">
<div class="col-xs-12 Lms-horizontal-center">
${show-more class="btn-primary Lms-show-more"}
</div>
</div>
</message>
<message id="Lms.Explore.Tracks.template.entry">
<div class="row Lms-explore-tracks-entry Lms-vertical-align">
<div class="col-xs-9">
<h4>${name}</h4>
</div>
<div class="col-sm-3 hidden-xs">
${<if-has-release>}<small>${release-name}</small>${</if-has-release>}
</div>
<div class="col-sm-3 hidden-xs">
${<if-has-artist>}<small><i>${artist-name}</i></small>${</if-has-artist>}
</div>
<div class="col-xs-3 Lms-explore-tracks-entry-controls">
${play-btn}${add-btn}
</div>
</div>
</message>
</messages>
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<!--FORMS message blocks-->
<message id="Lms.Explore.TracksInfo.template">
<h3>${tr:Lms.Explore.TracksInfo.most-played}</h3>
${most-played}
<h3>${tr:Lms.Explore.TracksInfo.recently-added}</h3>
${recently-added}
</message>
<message id="Lms.Explore.TracksInfo.template.entry">
<div class="row Lms-explore-tracksinfo-entry Lms-vertical-align">
<div class="col-xs-12">
${name}
</div>
</div>
</message>
</messages>
+24
View File
@@ -0,0 +1,24 @@
# LMS Sample configuration file
# Path to the working directory
# Must have write privileges in order to create and modify this directory
working-dir = "/var/lms/";
# Listen port/addr of the web server
listen-port = 5082;
listen-addr = "0.0.0.0";
behind-reverse-proxy = false;
# If enabled, these files have to exist and have correct permissions
tls-enable = false;
tls-cert = "/var/lms/cert.pem";
tls-key = "/var/lms/privkey.pem";
tls-dh = "/var/lms/dh2048.pem";
# Path to the resources used by the web interface
wt-resources="/usr/share/Wt/resources";
docroot = "/usr/share/lms/docroot/;/resources,/css,/images,/js,/favicon.ico";
approot = "/usr/share/lms/approot";
# Turn on this option to allow the demo account creation/use
#demo = false;
+8 -19
View File
@@ -8,14 +8,6 @@ AC_LANG_CPLUSPLUS
# Checks for programs
AC_PROG_CXX
AC_ARG_ENABLE([video], AS_HELP_STRING([--enable-video],
[Enable Video support. Default to no.])],
[enable_video="$enableval"],
[enable_video="no"])
AM_CONDITIONAL([VIDEO], [test x$enable_video = xyes])
AS_IF([test x$enable_video = xyes], [AC_DEFINE(HAVE_VIDEO, [1], [Enable Video support])])
PKG_CHECK_MODULES(IMAGEMAGICKXX, "ImageMagick++", [ HAVE_IMAGEMAGICKXX=yes ], [ ])
if test -n "$HAVE_IMAGEMAGICKXX"; then
MAGICKXX_CFLAGS="$IMAGEMAGICKXX_CFLAGS"
@@ -24,7 +16,7 @@ fi
AC_SUBST(MAGICKXX_CFLAGS)
AC_SUBST(MAGICKXX_LIBS)
AC_CHECK_HEADERS([Wt/WApplication pstreams/pstream.h],
AC_CHECK_HEADERS([Wt/WApplication.h pstreams/pstream.h],
[],
[AC_MSG_ERROR([Header not found or unusable !])])
@@ -74,24 +66,21 @@ AC_CHECK_LIB( [boost_filesystem],
,
[AC_MSG_ERROR([libboost_filesystem not found!])])
AC_CHECK_LIB( [boost_locale],
AC_CHECK_LIB( [tag],
[main],
,
[AC_MSG_ERROR([libboost_locale not found!])])
[AC_MSG_ERROR([libtag not found!])])
AC_CHECK_LIB( [boost_date_time],
AC_CHECK_LIB( [config++],
[main],
,
[AC_MSG_ERROR([libboost_date_time not found!])])
AC_CHECK_LIB( [boost_iostreams],
[main],
,
[AC_MSG_ERROR([libboost_iostreams not found!])])
[AC_MSG_ERROR([libconfig++ not found!])])
AC_CONFIG_FILES([Makefile
src/Makefile
test/Makefile])
test/Makefile
tools/Makefile
tools/metadata/Makefile])
AC_OUTPUT
+313 -195
View File
@@ -1,223 +1,341 @@
.main-nav {
margin-bottom: 0px;
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4);
}
.Wt-hrh2 {
background-color: white;
}
.Wt-vrh2 {
background-color: white;
}
.playqueue {
background-color: #EEE;
border-radius: 10px;
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4);
}
.playqueue-playing {
font-weight: bold;
}
.playqueue-track {
font-size: 120%;
margin-top: 12px;
line-height: normal;
}
.playqueue-artist {
line-height: normal;
font-style: italic;
}
.playqueue-cover {
width: 64px;
height: 64px;
}
.mediaplayer-track {
font-weight: bold;
font-size: 120%;
}
.mediaplayer-artist {
font-style: italic;
}
.mediaplayer {
background-color: #CCC;
border-radius: 10px;
display: flex;
justify-content: space-between;
align-items: center;
height: 72px;
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4);
}
.mediaplayer-cover {
margin: 8px;
width: 64px;
min-width: 64px;
height: 64px;
min-height: 64px;
display: flex;
justify-content: space-between;
align-items: center;
}
.mediaplayer-cover img {
border-radius: 8px;
border-radius: 8px;
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4);
}
.mediaplayer-btn {
margin: 8px;
}
.mediaplayer-btn i {
a, a:link, a:active, a:visited {
color: black;
text-decoration: none;
cursor: pointer;
text-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4);
}
.mediaplayer-btn-active {
color: #428bca;
a:hover {
color: #337ab7;
}
.mediaplayer-info-container {
width: 100%;
min-width: 100px;
margin: 8px;
}
.mediaplayer-track-info {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.mediaplayer-progress-container {
.Lms-vertical-align {
display: flex;
align-items: center;
}
.mediaplayer-badge {
min-width: initial;
.Lms-horizontal-center {
display: flex;
justify-content: center;
}
.mediaplayer-current-duration {
margin-right: 8px;
.Lms-show-more {
margin-top: 8px;
margin-bottom: 8px;
}
.mediaplayer-total-duration {
.Lms-admin-users-entry {
min-height: 48px;
padding-top: 8px;
padding-bottom: 8px;
}
.Lms-admin-users-add-btn {
margin-top: 8px;
margin-bottom: 8px;
}
.Lms-admin-users-entry:hover {
background-color: lightgrey;
}
.Lms-admin-users-entry-role {
margin-left: 8px;
}
.mediaplayer-seekbar {
.Lms-admin-users-entry-btn {
margin-right: 8px;
}
.mediaplayer-volume-container {
width: 24px;
height: 64px;
margin: 8px;
}
.mediaplayer-volume {
height: 64px;
-webkit-appearance: slider-vertical;
writing-mode: bt-lr;
}
.mobile-btn {
margin: 8px;
}
.mobile-btn-active {
color: #428bca;
}
.mobile-btn i {
.Lms-cluster {
display: inline;
cursor: pointer;
text-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4);
}
.mobile-search-title {
font-weight: bold;
height: 32px;
line-height: 32px;
background-color: grey;
color: white;
.Lms-cluster-type-0 {
background-color: darkslategrey;
margin-bottom: 4px;
}
.Lms-cluster-type-1 {
background-color: darkblue;
margin-bottom: 4px;
}
.Lms-cluster-type-2 {
background-color: darkgrey;
margin-bottom: 4px;
}
.Lms-cluster-type-3 {
background-color: darkorchid;
margin-bottom: 4px;
}
.Lms-cluster-type-4 {
background-color: darkgoldenrod;
margin-bottom: 4px;
}
.Lms-cluster-type-5 {
background-color: darkkhaki;
margin-bottom: 4px;
}
.Lms-cluster-type-6 {
background-color: darkolivegreen;
margin-bottom: 4px;
}
.Lms-cluster-type-7 {
background-color: darkred;
margin-bottom: 4px;
}
.Lms-cluster-type-8 {
background-color: darkviolet;
margin-bottom: 4px;
}
.Lms-cluster-type-9 {
background-color: darkturquoise;
margin-bottom: 4px;
}
.Lms-contents {
margin-bottom: 78px;
}
.Lms-explore-filters {
background-color: #f5f5f5;
padding: 8px;
border-radius: 8px;
margin-bottom: 16px;
}
.Lms-explore-info {
background-color: #f5f5f5;
padding: 8px;
border-radius: 8px;
}
.Lms-explore-releases-entry {
padding-top: 8px;
padding-bottom: 8px;
}
.Lms-explore-releases-entry:hover {
background-color: lightgrey;
}
.Lms-explore-releasesinfo-entry {
margin: 0px;
padding-top: 4px;
padding-bottom: 4px;
}
.Lms-explore-releasesinfo-entry:hover {
background-color: lightgrey;
}
.Lms-explore-release-cover {
max-width: 100%;
border-radius: 8px;
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4);
}
.Lms-explore-release-entry-name {
margin-top: 4px;
margin-bottom: 4px;
}
.Lms-explore-release-entry:hover {
background-color: lightgrey;
}
.Lms-explore-release-entry-controls {
text-align: center;
}
.mobile-search-entry {
min-height: 64px;
border-bottom: 1px solid lightgray;
.Lms-explore-artists-entry {
min-height: 48px;
}
.Lms-explore-artists-entry:hover {
background-color: lightgrey;
}
.Lms-explore-artistsinfo-entry {
min-height: 28px;
margin: 0px;
}
.Lms-explore-artistsinfo-entry:hover {
background-color: lightgrey;
}
.Lms-explore-artist-entry {
padding-top: 8px;
padding-bottom: 8px;
}
.Lms-explore-artist-entry:hover {
background-color: lightgrey;
}
.Lms-explore-tracks-entry {
min-height: 32px;
padding-top: 2px;
padding-bottom: 2px;
}
.Lms-explore-tracks-entry:hover {
background-color: lightgrey;
}
.Lms-explore-tracks-entry-controls {
text-align: center;
}
.Lms-explore-tracksinfo-entry {
min-height: 28px;
margin: 0px;
}
.Lms-explore-tracksinfo-entry:hover {
background-color: lightgrey;
}
.Lms-explore-btn {
font-size: 22px !important;
cursor: pointer;
}
.Lms-explore-btn:hover {
color: #337ab7;
}
.Lms-playqueue-entry {
min-height: 32px;
padding-top: 2px;
padding-bottom: 2px;
}
.Lms-playqueue-entry:hover {
background-color: lightgrey;
}
.Lms-playqueue-entry-controls {
text-align: center;
}
.Lms-playqueue-btn {
font-size: 22px !important;
cursor: pointer;
margin: 2px;
}
.Lms-playqueue-btn:hover {
color: #337ab7;
}
.Lms-playqueue-selected {
font-weight: 800;
}
.Lms-playqueue-selected h4 {
font-weight: inherit;
}
.Lms-playhistory-entry {
min-height: 32px;
margin-bottom: 2px;
}
.Lms-playhistory-entry:hover {
background-color: lightgrey;
}
.Lms-player {
position: fixed;
bottom: 0;
width: 100%;
height: 70px;
background-color: #f5f5f5;
box-shadow: 0px -2px 8px #f5f5f5;
}
.Lms-player-btn {
font-size: 34px !important;
cursor: pointer;
}
.Lms-player-btn:hover {
color: #337ab7;
}
.Lms-player-btn-playpause {
width: 32px;
margin-left: 8px;
margin-right: 8px;
}
.Lms-player-seek-container {
margin-top: 8px;
margin-bottom: 8px;
position: relative;
width: 100%;
height: 8px;
}
.Lms-player-seek-common {
position: absolute;
width:100%;
top: 0px;
left: 0px;
height: 8px;
}
.Lms-player-seek {
cursor: pointer;
z-index: 1;
touch-action: manipulation;
-webkit-appearance: none;
-moz-appearance: none;
background: transparent;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
-moz-appearance: none;
height: 0px;
width: 0px;
}
input[type="range"]::-moz-range-thumb {
-webkit-appearance: none;
-moz-appearance: none;
height: 0px;
width: 0px;
}
.Lms-player-progress {
background-color: darkgrey;
}
.Lms-player-controls {
text-align: center;
white-space: nowrap;
}
.Lms-player-text-center {
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mobile-search-entry:active {
background-color: lightgrey;
}
.mobile-search-more {
height: 64px;
border-bottom: 1px solid lightgray;
}
.mobile-search-more:active {
background-color: lightgrey;
}
.mobile-track {
font-weight: bold;
}
.mobile-artist {
font-style: italic;
}
.vertical-align {
display: flex;
align-items: center;
}
.mobile-audio-footer {
background-color: #f5f5f5;
height: 60px;
}
.mobile-audio-player-cover {
width: 48px;
height: 48px;
}
.release_res_shadow {
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.5)
}
.release_truncated-name {
white-space: nowrap;
overflow: hidden;
overflow-x: hidden;
overflow-y: hidden;
text-overflow: ellipsis;
}
.release_img-responsive {
display: block;
max-width: 100%;
height: auto;
}
.release-search-cover {
width: 512px;
min-width: 512px;
height: 512px;
min-height: 512px;
display: flex;
justify-content: space-between;
align-items: center;
.Lms-player-text {
margin-left:8px;
margin-right:8px;
}
+417
View File
@@ -0,0 +1,417 @@
/*
* Project: Bootstrap Notify = v3.1.5
* Description: Turns standard Bootstrap alerts into "Growl-like" notifications.
* Author: Mouse0270 aka Robert McIntosh
* License: MIT License
* Website: https://github.com/mouse0270/bootstrap-growl
*/
/* global define:false, require: false, jQuery:false */
(function (factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['jquery'], factory);
} else if (typeof exports === 'object') {
// Node/CommonJS
factory(require('jquery'));
} else {
// Browser globals
factory(jQuery);
}
}(function ($) {
// Create the defaults once
var defaults = {
element: 'body',
position: null,
type: "info",
allow_dismiss: true,
allow_duplicates: true,
newest_on_top: false,
showProgressbar: false,
placement: {
from: "top",
align: "right"
},
offset: 20,
spacing: 10,
z_index: 1031,
delay: 5000,
timer: 1000,
url_target: '_blank',
mouse_over: null,
animate: {
enter: 'animated fadeInDown',
exit: 'animated fadeOutUp'
},
onShow: null,
onShown: null,
onClose: null,
onClosed: null,
onClick: null,
icon_type: 'class',
template: '<div data-notify="container" class="col-xs-11 col-sm-4 alert alert-{0}" role="alert"><button type="button" aria-hidden="true" class="close" data-notify="dismiss">&times;</button><span data-notify="icon"></span> <span data-notify="title">{1}</span> <span data-notify="message">{2}</span><div class="progress" data-notify="progressbar"><div class="progress-bar progress-bar-{0}" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;"></div></div><a href="{3}" target="{4}" data-notify="url"></a></div>'
};
String.format = function () {
var args = arguments;
var str = arguments[0];
return str.replace(/(\{\{\d\}\}|\{\d\})/g, function (str) {
if (str.substring(0, 2) === "{{") return str;
var num = parseInt(str.match(/\d/)[0]);
return args[num + 1];
});
};
function isDuplicateNotification(notification) {
var isDupe = false;
$('[data-notify="container"]').each(function (i, el) {
var $el = $(el);
var title = $el.find('[data-notify="title"]').html().trim();
var message = $el.find('[data-notify="message"]').html().trim();
// The input string might be different than the actual parsed HTML string!
// (<br> vs <br /> for example)
// So we have to force-parse this as HTML here!
var isSameTitle = title === $("<div>" + notification.settings.content.title + "</div>").html().trim();
var isSameMsg = message === $("<div>" + notification.settings.content.message + "</div>").html().trim();
var isSameType = $el.hasClass('alert-' + notification.settings.type);
if (isSameTitle && isSameMsg && isSameType) {
//we found the dupe. Set the var and stop checking.
isDupe = true;
}
return !isDupe;
});
return isDupe;
}
function Notify(element, content, options) {
// Setup Content of Notify
var contentObj = {
content: {
message: typeof content === 'object' ? content.message : content,
title: content.title ? content.title : '',
icon: content.icon ? content.icon : '',
url: content.url ? content.url : '#',
target: content.target ? content.target : '-'
}
};
options = $.extend(true, {}, contentObj, options);
this.settings = $.extend(true, {}, defaults, options);
this._defaults = defaults;
if (this.settings.content.target === "-") {
this.settings.content.target = this.settings.url_target;
}
this.animations = {
start: 'webkitAnimationStart oanimationstart MSAnimationStart animationstart',
end: 'webkitAnimationEnd oanimationend MSAnimationEnd animationend'
};
if (typeof this.settings.offset === 'number') {
this.settings.offset = {
x: this.settings.offset,
y: this.settings.offset
};
}
//if duplicate messages are not allowed, then only continue if this new message is not a duplicate of one that it already showing
if (this.settings.allow_duplicates || (!this.settings.allow_duplicates && !isDuplicateNotification(this))) {
this.init();
}
}
$.extend(Notify.prototype, {
init: function () {
var self = this;
this.buildNotify();
if (this.settings.content.icon) {
this.setIcon();
}
if (this.settings.content.url != "#") {
this.styleURL();
}
this.styleDismiss();
this.placement();
this.bind();
this.notify = {
$ele: this.$ele,
update: function (command, update) {
var commands = {};
if (typeof command === "string") {
commands[command] = update;
} else {
commands = command;
}
for (var cmd in commands) {
switch (cmd) {
case "type":
this.$ele.removeClass('alert-' + self.settings.type);
this.$ele.find('[data-notify="progressbar"] > .progress-bar').removeClass('progress-bar-' + self.settings.type);
self.settings.type = commands[cmd];
this.$ele.addClass('alert-' + commands[cmd]).find('[data-notify="progressbar"] > .progress-bar').addClass('progress-bar-' + commands[cmd]);
break;
case "icon":
var $icon = this.$ele.find('[data-notify="icon"]');
if (self.settings.icon_type.toLowerCase() === 'class') {
$icon.removeClass(self.settings.content.icon).addClass(commands[cmd]);
} else {
if (!$icon.is('img')) {
$icon.find('img');
}
$icon.attr('src', commands[cmd]);
}
self.settings.content.icon = commands[command];
break;
case "progress":
var newDelay = self.settings.delay - (self.settings.delay * (commands[cmd] / 100));
this.$ele.data('notify-delay', newDelay);
this.$ele.find('[data-notify="progressbar"] > div').attr('aria-valuenow', commands[cmd]).css('width', commands[cmd] + '%');
break;
case "url":
this.$ele.find('[data-notify="url"]').attr('href', commands[cmd]);
break;
case "target":
this.$ele.find('[data-notify="url"]').attr('target', commands[cmd]);
break;
default:
this.$ele.find('[data-notify="' + cmd + '"]').html(commands[cmd]);
}
}
var posX = this.$ele.outerHeight() + parseInt(self.settings.spacing) + parseInt(self.settings.offset.y);
self.reposition(posX);
},
close: function () {
self.close();
}
};
},
buildNotify: function () {
var content = this.settings.content;
this.$ele = $(String.format(this.settings.template, this.settings.type, content.title, content.message, content.url, content.target));
this.$ele.attr('data-notify-position', this.settings.placement.from + '-' + this.settings.placement.align);
if (!this.settings.allow_dismiss) {
this.$ele.find('[data-notify="dismiss"]').css('display', 'none');
}
if ((this.settings.delay <= 0 && !this.settings.showProgressbar) || !this.settings.showProgressbar) {
this.$ele.find('[data-notify="progressbar"]').remove();
}
},
setIcon: function () {
if (this.settings.icon_type.toLowerCase() === 'class') {
this.$ele.find('[data-notify="icon"]').addClass(this.settings.content.icon);
} else {
if (this.$ele.find('[data-notify="icon"]').is('img')) {
this.$ele.find('[data-notify="icon"]').attr('src', this.settings.content.icon);
} else {
this.$ele.find('[data-notify="icon"]').append('<img src="' + this.settings.content.icon + '" alt="Notify Icon" />');
}
}
},
styleDismiss: function () {
this.$ele.find('[data-notify="dismiss"]').css({
position: 'absolute',
right: '10px',
top: '5px',
zIndex: this.settings.z_index + 2
});
},
styleURL: function () {
this.$ele.find('[data-notify="url"]').css({
backgroundImage: 'url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)',
height: '100%',
left: 0,
position: 'absolute',
top: 0,
width: '100%',
zIndex: this.settings.z_index + 1
});
},
placement: function () {
var self = this,
offsetAmt = this.settings.offset.y,
css = {
display: 'inline-block',
margin: '0px auto',
position: this.settings.position ? this.settings.position : (this.settings.element === 'body' ? 'fixed' : 'absolute'),
transition: 'all .5s ease-in-out',
zIndex: this.settings.z_index
},
hasAnimation = false,
settings = this.settings;
$('[data-notify-position="' + this.settings.placement.from + '-' + this.settings.placement.align + '"]:not([data-closing="true"])').each(function () {
offsetAmt = Math.max(offsetAmt, parseInt($(this).css(settings.placement.from)) + parseInt($(this).outerHeight()) + parseInt(settings.spacing));
});
if (this.settings.newest_on_top === true) {
offsetAmt = this.settings.offset.y;
}
css[this.settings.placement.from] = offsetAmt + 'px';
switch (this.settings.placement.align) {
case "left":
case "right":
css[this.settings.placement.align] = this.settings.offset.x + 'px';
break;
case "center":
css.left = 0;
css.right = 0;
break;
}
this.$ele.css(css).addClass(this.settings.animate.enter);
$.each(Array('webkit-', 'moz-', 'o-', 'ms-', ''), function (index, prefix) {
self.$ele[0].style[prefix + 'AnimationIterationCount'] = 1;
});
$(this.settings.element).append(this.$ele);
if (this.settings.newest_on_top === true) {
offsetAmt = (parseInt(offsetAmt) + parseInt(this.settings.spacing)) + this.$ele.outerHeight();
this.reposition(offsetAmt);
}
if ($.isFunction(self.settings.onShow)) {
self.settings.onShow.call(this.$ele);
}
this.$ele.one(this.animations.start, function () {
hasAnimation = true;
}).one(this.animations.end, function () {
self.$ele.removeClass(self.settings.animate.enter);
if ($.isFunction(self.settings.onShown)) {
self.settings.onShown.call(this);
}
});
setTimeout(function () {
if (!hasAnimation) {
if ($.isFunction(self.settings.onShown)) {
self.settings.onShown.call(this);
}
}
}, 600);
},
bind: function () {
var self = this;
this.$ele.find('[data-notify="dismiss"]').on('click', function () {
self.close();
});
if ($.isFunction(self.settings.onClick)) {
this.$ele.on('click', function (event) {
if (event.target != self.$ele.find('[data-notify="dismiss"]')[0]) {
self.settings.onClick.call(this, event);
}
});
}
this.$ele.mouseover(function () {
$(this).data('data-hover', "true");
}).mouseout(function () {
$(this).data('data-hover', "false");
});
this.$ele.data('data-hover', "false");
if (this.settings.delay > 0) {
self.$ele.data('notify-delay', self.settings.delay);
var timer = setInterval(function () {
var delay = parseInt(self.$ele.data('notify-delay')) - self.settings.timer;
if ((self.$ele.data('data-hover') === 'false' && self.settings.mouse_over === "pause") || self.settings.mouse_over != "pause") {
var percent = ((self.settings.delay - delay) / self.settings.delay) * 100;
self.$ele.data('notify-delay', delay);
self.$ele.find('[data-notify="progressbar"] > div').attr('aria-valuenow', percent).css('width', percent + '%');
}
if (delay <= -(self.settings.timer)) {
clearInterval(timer);
self.close();
}
}, self.settings.timer);
}
},
close: function () {
var self = this,
posX = parseInt(this.$ele.css(this.settings.placement.from)),
hasAnimation = false;
this.$ele.attr('data-closing', 'true').addClass(this.settings.animate.exit);
self.reposition(posX);
if ($.isFunction(self.settings.onClose)) {
self.settings.onClose.call(this.$ele);
}
this.$ele.one(this.animations.start, function () {
hasAnimation = true;
}).one(this.animations.end, function () {
$(this).remove();
if ($.isFunction(self.settings.onClosed)) {
self.settings.onClosed.call(this);
}
});
setTimeout(function () {
if (!hasAnimation) {
self.$ele.remove();
if ($.isFunction(self.settings.onClosed)) {
self.settings.onClosed.call(this);
}
}
}, 600);
},
reposition: function (posX) {
var self = this,
notifies = '[data-notify-position="' + this.settings.placement.from + '-' + this.settings.placement.align + '"]:not([data-closing="true"])',
$elements = this.$ele.nextAll(notifies);
if (this.settings.newest_on_top === true) {
$elements = this.$ele.prevAll(notifies);
}
$elements.each(function () {
$(this).css(self.settings.placement.from, posX);
posX = (parseInt(posX) + parseInt(self.settings.spacing)) + $(this).outerHeight();
});
}
});
$.notify = function (content, options) {
var plugin = new Notify(this, content, options);
return plugin.notify;
};
$.notifyDefaults = function (options) {
defaults = $.extend(true, {}, defaults, options);
return defaults;
};
$.notifyClose = function (selector) {
if (typeof selector === "undefined" || selector === "all") {
$('[data-notify]').find('[data-notify="dismiss"]').trigger('click');
}else if(selector === 'success' || selector === 'info' || selector === 'warning' || selector === 'danger'){
$('.alert-' + selector + '[data-notify]').find('[data-notify="dismiss"]').trigger('click');
} else if(selector){
$(selector + '[data-notify]').find('[data-notify="dismiss"]').trigger('click');
}
else {
$('[data-notify-position="' + selector + '"]').find('[data-notify="dismiss"]').trigger('click');
}
};
$.notifyCloseExcept = function (selector) {
if(selector === 'success' || selector === 'info' || selector === 'warning' || selector === 'danger'){
$('[data-notify]').not('.alert-' + selector).find('[data-notify="dismiss"]').trigger('click');
} else{
$('[data-notify]').not(selector).find('[data-notify="dismiss"]').trigger('click');
}
};
}));
+6
View File
File diff suppressed because one or more lines are too long
+129
View File
@@ -0,0 +1,129 @@
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
var LMS = LMS || {};
LMS.mediaplayer = function () {
var _root = {};
var _elems = {};
var _offset = 0;
var _duration = 0;
var _audioSrc;
var _updateControls = function() {
_elems.progress.classList.toggle("active");
if (_elems.audio.paused) {
_elems.play.style.display = "inline";
_elems.pause.style.display = "none";
}
else {
_elems.pause.style.display = "inline";
_elems.play.style.display = "none";
_elems.progress.classList.add("active");
}
}
var _durationToString = function (duration, displayHours) {
var hours = parseInt(duration / 3600, 10) % 24;
var minutes = parseInt(duration / 60, 10) % 60;
var seconds = parseInt(duration, 10) % 60;
var res = "";
if (displayHours)
res += (hours < 10 ? "0" + hours : hours) + ":";
res += (minutes < 10 ? "0" + minutes : minutes) + ":";
res += (seconds < 10 ? "0" + seconds : seconds);
return res;
}
var _playTrack = function() {
var playPromise = _elems.audio.play();
if (playPromise !== undefined) {
playPromise.then(_ => {
// Automatic playback started
})
.catch(error => {
// Auto-play was prevented
});
}
}
var init = function(root) {
_root = root;
_elems.audio = document.getElementById("lms-mp-audio");
_elems.play = document.getElementById("lms-mp-play");
_elems.pause = document.getElementById("lms-mp-pause");
_elems.previous = document.getElementById("lms-mp-previous");
_elems.next = document.getElementById("lms-mp-next");
_elems.progress = document.getElementById("lms-mp-progress");
_elems.seek = document.getElementById("lms-mp-seek");
_elems.play.addEventListener("click", function() {
if (_elems.audio.hasAttribute("src"))
_elems.audio.play();
});
_elems.pause.addEventListener("click", function() {
_elems.audio.pause();
});
_elems.previous.addEventListener("click", function() {
Wt.emit(_root, "playPrevious");
});
_elems.next.addEventListener("click", function() {
Wt.emit(_root, "playNext");
});
_elems.seek.addEventListener("change", function() {
if (!_elems.audio.hasAttribute("src"))
return;
_offset = parseInt(_elems.seek.value, 10);
_elems.audio.src = _audioSrc + "&offset=" + _offset;
_elems.audio.load();
_playTrack();
});
_elems.audio.addEventListener("play", _updateControls);
_elems.audio.addEventListener("playing", _updateControls);
_elems.audio.addEventListener("pause", _updateControls);
_elems.audio.addEventListener("timeupdate", function() {
_elems.progress.style.width = "" + ((_offset + _elems.audio.currentTime) / _duration) * 100 + "%";
});
_elems.audio.addEventListener("ended", function() {
Wt.emit(_root, "playbackEnded");
});
}
var loadTrack = function(params, autoplay) {
console.log("Loading track, resource = '" + params.resource + "', imgResource = '" + params.imgResource + "'");
_offset = 0;
_duration = params.duration;
_audioSrc = params.resource;
_elems.seek.max = _duration;
_elems.audio.src = _audioSrc;
if (autoplay)
_playTrack();
}
var stop = function() {
_elems.audio.pause();
}
return {
init: init,
loadTrack: loadTrack,
stop: stop,
};
}();
// @license-end
+32 -51
View File
@@ -3,69 +3,50 @@ bin_PROGRAMS = lms
lms_SOURCES = \
$(srcdir)/main/main.cpp \
$(srcdir)/av/AvInfo.cpp \
$(srcdir)/av/AvTranscoder.cpp \
$(srcdir)/av/AvTranscoder.cpp \
$(srcdir)/cover/CoverArtGrabber.cpp \
$(srcdir)/database/Artist.cpp \
$(srcdir)/database/Cluster.cpp \
$(srcdir)/database/DatabaseHandler.cpp \
$(srcdir)/database/MediaDirectory.cpp \
$(srcdir)/database/Playlist.cpp \
$(srcdir)/database/TrackList.cpp \
$(srcdir)/database/Release.cpp \
$(srcdir)/database/SearchFilter.cpp \
$(srcdir)/database/ScanSettings.cpp \
$(srcdir)/database/SqlQuery.cpp \
$(srcdir)/database/Track.cpp \
$(srcdir)/database/User.cpp \
$(srcdir)/database/Video.cpp \
$(srcdir)/database-updater/DatabaseUpdater.cpp \
$(srcdir)/database-updater/Checksum.cpp \
$(srcdir)/image/Image.cpp \
$(srcdir)/logger/Logger.cpp \
$(srcdir)/metadata/AvFormat.cpp \
$(srcdir)/service/ServiceManager.cpp \
$(srcdir)/service/DatabaseUpdateService.cpp \
$(srcdir)/metadata/TagLibParser.cpp \
$(srcdir)/scanner/MediaScanner.cpp \
$(srcdir)/ui/Auth.cpp \
$(srcdir)/ui/LmsApplication.cpp \
$(srcdir)/ui/auth/LmsAuth.cpp \
$(srcdir)/ui/audio/AudioPlayer.cpp \
$(srcdir)/ui/audio/desktop/DesktopAudio.cpp \
$(srcdir)/ui/audio/desktop/FilterChain.cpp \
$(srcdir)/ui/audio/desktop/KeywordSearchFilter.cpp \
$(srcdir)/ui/audio/desktop/PlayQueue.cpp \
$(srcdir)/ui/audio/desktop/TableFilter.cpp \
$(srcdir)/ui/audio/desktop/TrackView.cpp \
$(srcdir)/ui/audio/mobile/ArtistSearch.cpp \
$(srcdir)/ui/audio/mobile/ArtistSearchView.cpp \
$(srcdir)/ui/audio/mobile/ArtistView.cpp \
$(srcdir)/ui/audio/mobile/MobileAudio.cpp \
$(srcdir)/ui/audio/mobile/MobilePlayQueue.cpp \
$(srcdir)/ui/audio/mobile/PreviewSearchView.cpp \
$(srcdir)/ui/audio/mobile/ReleaseSearch.cpp \
$(srcdir)/ui/audio/mobile/ReleaseSearchView.cpp \
$(srcdir)/ui/audio/mobile/ReleaseView.cpp \
$(srcdir)/ui/audio/mobile/TrackSearch.cpp \
$(srcdir)/ui/audio/mobile/TrackSearchView.cpp \
$(srcdir)/ui/common/DirectoryValidator.cpp \
$(srcdir)/ui/common/InputRange.cpp \
$(srcdir)/ui/common/LineEdit.cpp \
$(srcdir)/ui/resource/AvConvTranscodeStreamResource.cpp \
$(srcdir)/ui/LmsApplicationGroup.cpp \
$(srcdir)/ui/MediaPlayer.cpp \
$(srcdir)/ui/PlayQueueView.cpp \
$(srcdir)/ui/PlayHistoryView.cpp \
$(srcdir)/ui/SettingsView.cpp \
$(srcdir)/ui/admin/DatabaseSettingsView.cpp \
$(srcdir)/ui/admin/InitWizardView.cpp \
$(srcdir)/ui/admin/UserView.cpp \
$(srcdir)/ui/admin/UsersView.cpp \
$(srcdir)/ui/common/Validators.cpp \
$(srcdir)/ui/explore/ArtistView.cpp \
$(srcdir)/ui/explore/ArtistsInfoView.cpp \
$(srcdir)/ui/explore/ArtistsView.cpp \
$(srcdir)/ui/explore/Explore.cpp \
$(srcdir)/ui/explore/Filters.cpp \
$(srcdir)/ui/explore/ReleasesInfoView.cpp \
$(srcdir)/ui/explore/ReleasesView.cpp \
$(srcdir)/ui/explore/ReleaseView.cpp \
$(srcdir)/ui/explore/TracksInfoView.cpp \
$(srcdir)/ui/explore/TracksView.cpp \
$(srcdir)/ui/resource/ImageResource.cpp \
$(srcdir)/ui/resource/TranscodeResource.cpp \
$(srcdir)/ui/settings/Settings.cpp \
$(srcdir)/ui/settings/SettingsAccountFormView.cpp \
$(srcdir)/ui/settings/SettingsAudioFormView.cpp \
$(srcdir)/ui/settings/SettingsDatabaseFormView.cpp \
$(srcdir)/ui/settings/SettingsFirstConnectionFormView.cpp \
$(srcdir)/ui/settings/SettingsMediaDirectories.cpp \
$(srcdir)/ui/settings/SettingsMediaDirectoryFormView.cpp \
$(srcdir)/ui/settings/SettingsUserFormView.cpp \
$(srcdir)/ui/settings/SettingsUsers.cpp \
$(srcdir)/utils/Config.cpp \
$(srcdir)/utils/Logger.cpp \
$(srcdir)/utils/Path.cpp \
$(srcdir)/utils/Utils.cpp
if VIDEO
lms_SOURCES += \
$(srcdir)/ui/video/VideoWidget.cpp \
$(srcdir)/ui/video/VideoDatabaseWidget.cpp \
$(srcdir)/ui/video/VideoMediaPlayerWidget.cpp \
$(srcdir)/ui/video/VideoParametersDialog.cpp
endif
lms_CXXFLAGS=-std=c++11 -Wall -I$(top_srcdir)/third-party -I$(srcdir)/ui $(MAGICKXX_CFLAGS)
lms_CXXFLAGS=-std=c++14 -Wall -I$(srcdir)/ui $(MAGICKXX_CFLAGS) -D_REENTRANT
lms_LDADD=$(MAGICKXX_LIBS)
+43 -115
View File
@@ -17,26 +17,14 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <array>
#include "logger/Logger.hpp"
#include "AvInfo.hpp"
#include <array>
#include "utils/Logger.hpp"
namespace Av {
static std::string streamType_to_string(Stream::Type type)
{
switch (type)
{
case Stream::Type::Audio: return "audio";
case Stream::Type::Video: return "video";
case Stream::Type::Subtitle: return "subtitle";
}
return "unknown";
}
static std::string averror_to_string(int error)
{
std::array<char, 128> buf = {0};
@@ -47,6 +35,10 @@ static std::string averror_to_string(int error)
return "Unknown error";
}
MediaFileException::MediaFileException(int avError)
: AvException("MediaFileException: " + averror_to_string(avError))
{
}
void AvInit()
{
@@ -62,56 +54,34 @@ void AvInit()
MediaFile::MediaFile(const boost::filesystem::path& p)
: _p(p), _context(nullptr)
{
int error = avformat_open_input(&_context, _p.string().c_str(), nullptr, nullptr);
if (error < 0)
{
LMS_LOG(AV, ERROR) << "Cannot open " << _p.string() << ": " << averror_to_string(error);
throw MediaFileException(error);
}
error = avformat_find_stream_info(_context, nullptr);
if (error < 0)
{
LMS_LOG(AV, ERROR) << "Cannot find stream information on " << _p.string() << ": " << averror_to_string(error);
avformat_close_input(&_context);
throw MediaFileException(error);
}
}
MediaFile::~MediaFile()
{
if (_context != nullptr)
avformat_close_input(&_context);
avformat_close_input(&_context);
}
bool
MediaFile::open(void)
{
if (_context != nullptr)
throw std::logic_error("inputfile '" + _p.string() + "' already open");
int error = avformat_open_input(&_context, _p.string().c_str(), nullptr, nullptr);
if (error < 0)
{
LMS_LOG(AV, ERROR) << "Cannot open '" << _p.string() << "': " << averror_to_string(error);
return false;
}
return true;
}
bool
MediaFile::scan(void)
{
if (_context == nullptr)
throw std::logic_error("inputfile '" + _p.string() + "' not open");
int error = avformat_find_stream_info(_context, nullptr);
if (error < 0)
{
LMS_LOG(AV, ERROR) << "Cannot find stream information on '" << _p.string() << "': " << averror_to_string(error);
return false;
}
return true;
}
boost::posix_time::time_duration
std::chrono::milliseconds
MediaFile::getDuration() const
{
if (_context == nullptr)
throw std::logic_error("inputfile '" + _p.string() + "' not open");
if (_context->duration == AV_NOPTS_VALUE)
return std::chrono::milliseconds(0); // TODO estimate
if (static_cast<int>(_context->duration) != AV_NOPTS_VALUE )
return boost::posix_time::seconds(_context->duration / AV_TIME_BASE);
else
return boost::posix_time::seconds(0); // TODO, do something better?
return std::chrono::milliseconds(_context->duration / AV_TIME_BASE * 1000);
}
void
@@ -130,9 +100,6 @@ getMetaDataFromDictionnary(AVDictionary* dictionnary, std::map<std::string, std:
std::map<std::string, std::string>
MediaFile::getMetaData(void)
{
if (_context == nullptr)
throw std::logic_error("inputfile '" + _p.string() + "' not open");
std::map<std::string, std::string> res;
getMetaDataFromDictionnary(_context->metadata, res);
@@ -153,13 +120,10 @@ MediaFile::getMetaData(void)
return res;
}
std::vector<Stream>
MediaFile::getStreams(Stream::Type type) const
std::vector<StreamInfo>
MediaFile::getStreamInfo() const
{
if (_context == nullptr)
throw std::logic_error("inputfile '" + _p.string() + "' not open");
std::vector<Stream> res;
std::vector<StreamInfo> res;
for (std::size_t i = 0; i < _context->nb_streams; ++i)
{
@@ -169,66 +133,33 @@ MediaFile::getStreams(Stream::Type type) const
if (avstream->disposition & AV_DISPOSITION_ATTACHED_PIC)
continue;
if (avstream->codec == nullptr)
if (!avstream->codecpar)
{
LMS_LOG(AV, ERROR) << "Skipping stream " << i << " since no codec is set";
LMS_LOG(AV, ERROR) << "Skipping stream " << i << " since no codecpar is set";
continue;
}
if (type == Stream::Type::Audio && avstream->codec->codec_type != AVMEDIA_TYPE_AUDIO)
continue;
else if (type == Stream::Type::Video && avstream->codec->codec_type != AVMEDIA_TYPE_VIDEO)
continue;
else if (type == Stream::Type::Subtitle && avstream->codec->codec_type != AVMEDIA_TYPE_SUBTITLE)
if (avstream->codecpar->codec_type != AVMEDIA_TYPE_AUDIO)
continue;
Stream stream;
stream.id = i; // or use stream->id ?
stream.type = type;
stream.bitrate = avstream->codec->bit_rate;
{
std::array<char, 256> buf = {0};
avcodec_string(buf.data(), buf.size(), avstream->codec, 0);
stream.desc = buf.data();
}
res.push_back(stream);
res.push_back( {.id = i, .bitrate = static_cast<std::size_t>(avstream->codecpar->bit_rate)} );
}
return res;
}
int
MediaFile::getBestStreamId(Stream::Type type) const
boost::optional<std::size_t>
MediaFile::getBestStream() const
{
if (_context == nullptr)
throw std::logic_error("inputfile '" + _p.string() + "' not open");
enum AVMediaType avMediaType;
switch (type)
{
case Stream::Type::Audio: avMediaType = AVMEDIA_TYPE_AUDIO; break;
case Stream::Type::Video: avMediaType = AVMEDIA_TYPE_VIDEO; break;
case Stream::Type::Subtitle: avMediaType = AVMEDIA_TYPE_SUBTITLE; break;
default:
return -1;
}
int res = av_find_best_stream(_context,
avMediaType,
AVMEDIA_TYPE_AUDIO,
-1, // Auto
-1, // Auto
NULL,
0);
if (res < 0)
{
LMS_LOG(AV, ERROR) << "Cannot find best stream for type " << streamType_to_string(type);
return -1;
}
return boost::none;
return res;
}
@@ -248,9 +179,6 @@ MediaFile::hasAttachedPictures(void) const
std::vector<Picture>
MediaFile::getAttachedPictures(std::size_t nbMaxPictures) const
{
if (_context == nullptr)
throw std::logic_error("inputfile '" + _p.string() + "' not open");
static const std::map<int, std::string> codecMimeMap =
{
{ AV_CODEC_ID_BMP, "image/x-bmp" },
@@ -271,15 +199,15 @@ MediaFile::getAttachedPictures(std::size_t nbMaxPictures) const
if (!(avstream->disposition & AV_DISPOSITION_ATTACHED_PIC))
continue;
if (avstream->codec == nullptr)
if (avstream->codecpar == nullptr)
{
LMS_LOG(AV, ERROR) << "Skipping stream " << i << " since no codec is set";
LMS_LOG(AV, ERROR) << "Skipping stream " << i << " since no codecpar is set";
continue;
}
Picture picture;
auto itMime = codecMimeMap.find(avstream->codec->codec_id);
auto itMime = codecMimeMap.find(avstream->codecpar->codec_id);
if (itMime != codecMimeMap.end())
{
picture.mimeType = itMime->second;
@@ -287,7 +215,7 @@ MediaFile::getAttachedPictures(std::size_t nbMaxPictures) const
else
{
picture.mimeType = "application/octet-stream";
LMS_LOG(AV, ERROR) << "CODEC ID " << avstream->codec->codec_id << " not handled in mime type conversion";
LMS_LOG(AV, ERROR) << "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion";
}
AVPacket pkt = avstream->attached_pic;
+23 -25
View File
@@ -19,8 +19,7 @@
/* This file contains some classes in order to get info from file using the libavconv */
#ifndef AV_INFO_HPP
#define AV_INFO_HPP
#pragma once
extern "C"
{
@@ -34,9 +33,12 @@ extern "C"
#include <string>
#include <cstdint>
#include <map>
#include <chrono>
#include <boost/optional.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/date_time/posix_time/posix_time_types.hpp> //no i/o just types
#include "utils/Exception.hpp"
namespace Av
{
@@ -49,25 +51,27 @@ struct Picture
std::vector<uint8_t> data;
};
struct Stream
struct StreamInfo
{
enum class Type
{
Audio,
Video,
Subtitle,
};
int id;
Type type;
size_t id;
std::size_t bitrate;
std::string desc; // Description of the stream
};
class AvException : public LmsException
{
public:
AvException(const std::string& msg) : LmsException(msg) {}
};
class MediaFileException : public AvException
{
public:
MediaFileException(int avError);
};
class MediaFile
{
public:
MediaFile(const boost::filesystem::path& p);
~MediaFile();
@@ -75,16 +79,13 @@ class MediaFile
MediaFile(const MediaFile&) = delete;
MediaFile& operator=(const MediaFile&) = delete;
boost::filesystem::path getPath() const {return _p;};
const boost::filesystem::path& getPath() const {return _p;};
bool open(void);
bool scan(void);
boost::posix_time::time_duration getDuration() const;
std::chrono::milliseconds getDuration() const;
std::map<std::string, std::string> getMetaData(void);
std::vector<Stream> getStreams(Stream::Type type) const;
int getBestStreamId(Stream::Type type) const; // -1 if failure/unknown
std::vector<StreamInfo> getStreamInfo() const;
boost::optional<std::size_t> getBestStream() const; // none if failure/unknown
bool hasAttachedPictures(void) const;
std::vector<Picture> getAttachedPictures(std::size_t nbMaxPictures) const;
@@ -95,8 +96,5 @@ class MediaFile
AVFormatContext* _context;
};
} // namespace Av
#endif
+29 -97
View File
@@ -16,14 +16,14 @@
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AvTranscoder.hpp"
#include <atomic>
#include <mutex>
#include <boost/tokenizer.hpp>
#include "logger/Logger.hpp"
#include "AvTranscoder.hpp"
#include "utils/Path.hpp"
#include "utils/Logger.hpp"
namespace Av {
@@ -40,14 +40,11 @@ static std::vector<EncodingInfo> encodingInfos =
{
{Encoding::MP3, "audio/mp3", 0},
{Encoding::OGA, "audio/ogg", 1},
{Encoding::OGV, "video/ogg", 2},
{Encoding::WEBMA, "audio/webm", 3},
{Encoding::WEBMV, "video/webm", 4},
{Encoding::M4A, "audio/mp4", 5},
{Encoding::M4V, "video/mp4", 6},
};
std::string encoding_to_mimetype(Encoding encoding)
std::string encodingToMimetype(Encoding encoding)
{
for (auto encodingInfo : encodingInfos)
{
@@ -55,10 +52,10 @@ std::string encoding_to_mimetype(Encoding encoding)
return encodingInfo.mimetype;
}
throw std::logic_error("encoding_to_mimetype failed!");
throw AvException("Invalid encoding");
}
int encoding_to_int(Encoding encoding)
int encodingToInt(Encoding encoding)
{
for (auto encodingInfo : encodingInfos)
{
@@ -66,10 +63,10 @@ int encoding_to_int(Encoding encoding)
return encodingInfo.id;
}
throw std::logic_error("encoding_to_int failed!");
throw AvException("Invalid encoding");
}
Encoding encoding_from_int(int encodingId)
Encoding encodingFromInt(int encodingId)
{
for (auto encodingInfo : encodingInfos)
{
@@ -77,7 +74,7 @@ Encoding encoding_from_int(int encodingId)
return encodingInfo.encoding;
}
throw std::logic_error("encoding_from_int failed!");
throw AvException("Invalid encodingId");
}
// TODO, parametrize?
@@ -91,37 +88,12 @@ static std::mutex transcoderMutex;
static boost::filesystem::path avConvPath = boost::filesystem::path();
static std::atomic<size_t> globalId = {0};
static std::string searchPath(std::string filename)
{
std::string path;
path = ::getenv("PATH");
if (path.empty())
throw std::runtime_error("Environment variable PATH not found");
std::string result;
typedef boost::tokenizer<boost::char_separator<char> > tokenizer;
boost::char_separator<char> sep(":");
tokenizer tok(path, sep);
for (tokenizer::iterator it = tok.begin(); it != tok.end(); ++it)
{
boost::filesystem::path p = *it;
p /= filename;
if (!::access(p.c_str(), X_OK))
{
result = p.string();
break;
}
}
return result;
}
void
Transcoder::init()
{
for (std::string execName : execNames)
{
boost::filesystem::path p = searchPath(execName);
boost::filesystem::path p = searchExecPath(execName);
if (!p.empty())
{
avConvPath = p;
@@ -130,9 +102,9 @@ Transcoder::init()
}
if (!avConvPath.empty())
LMS_LOG(TRANSCODE, INFO) << "Using transcoder " << avConvPath;
LMS_LOG(TRANSCODE, INFO) << "Using transcoder " << avConvPath.string();
else
throw std::runtime_error("Cannot find any transcoder binary!");
throw AvException("Cannot find any transcoder binary!");
}
Transcoder::Transcoder(boost::filesystem::path filePath, TranscodeParameters parameters)
@@ -152,7 +124,7 @@ Transcoder::start()
else if (!boost::filesystem::is_regular( _filePath) )
return false;
LMS_LOG_TRANSCODE(INFO) << "Transcoding file '" << _filePath << "'";
LMS_LOG_TRANSCODE(INFO) << "Transcoding file '" << _filePath.string() << "'";
std::vector<std::string> args;
@@ -165,10 +137,10 @@ Transcoder::start()
args.push_back("-nostdin");
// input Offset
if (_parameters.getOffset().total_seconds() > 0)
if (_parameters.offset)
{
args.push_back("-ss");
args.push_back(std::to_string(_parameters.getOffset().total_seconds()));
args.push_back(std::to_string((*_parameters.offset).count()));
}
// Input file
@@ -177,20 +149,24 @@ Transcoder::start()
// Output bitrates
args.push_back("-b:a");
args.push_back(std::to_string(_parameters.getBitrate(Stream::Type::Audio)));
// if (_parameters.getOutputFormat().getType() == Format::Video)
// oss << " -b:v " << _parameters.getOutputBitrate(Stream::Video);
args.push_back(std::to_string(_parameters.bitrate));
// Stream mapping
for (int streamId : _parameters.getSelectedStreamIds())
// Stream mapping, if set
if (_parameters.stream)
{
// 0 means the first input file
args.push_back("-map");
args.push_back("0:" + std::to_string(streamId));
args.push_back("0:" + std::to_string(*_parameters.stream));
}
// Strip metadata
args.push_back("-map_metadata");
args.push_back("-1");
// Skip video flows (including covers)
args.push_back("-vn");
// Codecs and formats
switch( _parameters.getEncoding())
switch( _parameters.encoding)
{
case Encoding::MP3:
args.push_back("-f");
@@ -204,20 +180,6 @@ Transcoder::start()
args.push_back("ogg");
break;
case Encoding::OGV:
args.push_back("-acodec");
args.push_back("libvorbis");
args.push_back("-ac");
args.push_back("2");
args.push_back("-ar");
args.push_back("44100");
args.push_back("-vcodec");
args.push_back("libtheora");
args.push_back("-threads");
args.push_back("4");
args.push_back("-f");
args.push_back("ogg");
break;
case Encoding::WEBMA:
args.push_back("-codec:a");
args.push_back("libvorbis");
@@ -225,21 +187,6 @@ Transcoder::start()
args.push_back("webm");
break;
case Encoding::WEBMV:
args.push_back("-acodec");
args.push_back("libvorbis");
args.push_back("-ac");
args.push_back("2");
args.push_back("-ar");
args.push_back("44100");
args.push_back("-vcodec");
args.push_back("libvpx");
args.push_back("-threads");
args.push_back("4");
args.push_back("-f");
args.push_back("webm");
break;
case Encoding::M4A:
args.push_back("-acodec");
args.push_back("aac");
@@ -249,21 +196,6 @@ Transcoder::start()
args.push_back("experimental");
break;
case Encoding::M4V:
args.push_back("-acodec");
args.push_back("aac");
args.push_back("-strict");
args.push_back("experimental");
args.push_back("-ac");
args.push_back("2");
args.push_back("-ar");
args.push_back("-44100");
args.push_back("-vcodec");
args.push_back("libx264");
args.push_back("-f");
args.push_back("m4v");
break;
default:
return false;
}
+12 -32
View File
@@ -22,11 +22,10 @@
#include <map>
#include <set>
#include "pstreams/pstream.h"
#include <pstreams/pstream.h>
#include <boost/date_time/posix_time/posix_time_types.hpp> //no i/o just types
#include <boost/filesystem/path.hpp>
#include <boost/filesystem.hpp>
#include <boost/optional.hpp>
#include "AvInfo.hpp"
@@ -39,42 +38,23 @@ enum class Encoding
OGV,
MP3,
WEBMA,
WEBMV,
M4A,
M4V,
};
std::string encoding_to_mimetype(Encoding encoding);
int encoding_to_int(Encoding encoding);
Encoding encoding_from_int(int encoding);
std::string encodingToMimetype(Encoding encoding);
int encodingToInt(Encoding encoding);
Encoding encodingFromInt(int encoding);
class TranscodeParameters
struct TranscodeParameters
{
public:
Encoding encoding = Encoding::MP3;
std::size_t bitrate = 128000;
boost::optional<std::size_t> stream = boost::none; // Id of the stream to be transcoded (auto detect by default)
boost::optional<std::chrono::seconds> offset;
// Setters
void setEncoding(Encoding encoding) { _encoding = encoding; }
void setOffset(boost::posix_time::time_duration offset) {_offset = offset; }
void setBitrate(Stream::Type type, std::size_t bitrate) { _outputBitrate[type] = bitrate; }
// Manually add the streams to be transcoded
// If no stream is added, input streams are selected automatically
void addStream(int inputStreamId) { _selectedStreams.insert(inputStreamId); }
// Getters
Encoding getEncoding(void) const { return _encoding; }
boost::posix_time::time_duration getOffset(void) const { return _offset; }
std::set<int> getSelectedStreamIds(void) const { return _selectedStreams; }
std::size_t getBitrate(Stream::Type type) { return _outputBitrate[type]; }
private:
Encoding _encoding = Encoding::MP3;
boost::posix_time::time_duration _offset = boost::posix_time::seconds(0);
std::set<int> _selectedStreams;
std::map<Stream::Type, std::size_t> _outputBitrate = { {Stream::Type::Audio, 0}, { Stream::Type::Video, 0}, { Stream::Type::Subtitle, 0} };
TranscodeParameters() {}
};
class Transcoder
{
public:
@@ -89,7 +69,7 @@ class Transcoder
bool start();
void process(std::vector<unsigned char>& output, std::size_t maxSize);
bool isComplete(void) { return _isComplete; }
bool isComplete(void) const { return _isComplete; }
const TranscodeParameters& getParameters() const { return _parameters; }
+26 -18
View File
@@ -17,10 +17,16 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "logger/Logger.hpp"
#include "CoverArtGrabber.hpp"
#include <boost/filesystem.hpp>
#include "av/AvInfo.hpp"
#include "CoverArtGrabber.hpp"
#include "database/Release.hpp"
#include "database/Track.hpp"
#include "utils/Logger.hpp"
namespace {
@@ -64,7 +70,7 @@ getFromAvMediaFile(const Av::MediaFile& input, std::size_t nbMaxCovers)
if (image.load(picture.data))
res.push_back( image );
else
LMS_LOG(COVER, ERROR) << "Cannot load embedded cover file in '" << input.getPath() << "'";
LMS_LOG(COVER, ERROR) << "Cannot load embedded cover file in '" << input.getPath().string() << "'";
}
return res;
@@ -86,7 +92,7 @@ Grabber::getFromDirectory(const boost::filesystem::path& p, std::size_t nbMaxCov
if (image.load(coverPath))
res.push_back(image);
else
LMS_LOG(COVER, ERROR) << "Cannot load image in file '" << coverPath << "'";
LMS_LOG(COVER, ERROR) << "Cannot load image in file '" << coverPath.string() << "'";
}
return res;
@@ -115,7 +121,7 @@ Grabber::getCoverPaths(const boost::filesystem::path& directoryPath, std::size_t
if (boost::filesystem::file_size(path) > _maxFileSize)
{
LMS_LOG(COVER, INFO) << "Cover file '" << path << " is too big (" << boost::filesystem::file_size(path) << "), limit is " << _maxFileSize;
LMS_LOG(COVER, INFO) << "Cover file '" << path.string() << " is too big (" << boost::filesystem::file_size(path) << "), limit is " << _maxFileSize;
continue;
}
@@ -129,16 +135,22 @@ Grabber::getCoverPaths(const boost::filesystem::path& directoryPath, std::size_t
std::vector<Image::Image>
Grabber::getFromTrack(const boost::filesystem::path& p, std::size_t nbMaxCovers) const
{
Av::MediaFile input(p);
try
{
Av::MediaFile input(p);
if (input.open())
return getFromAvMediaFile(input, nbMaxCovers);
else
return std::vector<Image::Image>();
}
catch (Av::MediaFileException& e)
{
LMS_LOG(COVER, ERROR) << "Cannot get covers from track " << p.string() << ": " << e.what();
}
return std::vector<Image::Image>();
}
std::vector<Image::Image>
Grabber::getFromTrack(Wt::Dbo::Session& session, Database::Track::id_type trackId, std::size_t nbMaxCovers) const
Grabber::getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, std::size_t nbMaxCovers) const
{
using namespace Database;
@@ -166,22 +178,18 @@ Grabber::getFromTrack(Wt::Dbo::Session& session, Database::Track::id_type trackI
std::vector<Image::Image>
Grabber::getFromRelease(Wt::Dbo::Session& session, Database::Release::id_type releaseId, std::size_t nbMaxCovers) const
Grabber::getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, std::size_t nbMaxCovers) const
{
using namespace Database;
Wt::Dbo::Transaction transaction(session);
// If the release does not exist or is the special release "None", do nothing
// If the release does not exist, do nothing
Release::pointer release = Release::getById(session, releaseId);
if (!release || release->isNone())
if (!release)
return std::vector<Image::Image>();
// Get the first track of the release
std::vector<Track::pointer> tracks = Track::getByFilter(session,
SearchFilter::ById(SearchFilter::Field::Release, releaseId),
-1, 1 /* limit result size */);
std::vector<Track::pointer> tracks = release->getTracks();
if (tracks.empty())
return std::vector<Image::Image>();
+2 -2
View File
@@ -38,8 +38,8 @@ class Grabber
std::vector<boost::filesystem::path> getCoverPaths(const boost::filesystem::path& directoryPath, std::size_t nbMaxCovers = 1) const;
std::vector<Image::Image> getFromDirectory(const boost::filesystem::path& path, std::size_t nbMaxCovers = 1) const;
std::vector<Image::Image> getFromTrack(const boost::filesystem::path& path, std::size_t nbMaxCovers = 1) const;
std::vector<Image::Image> getFromTrack(Wt::Dbo::Session& session, Database::Track::id_type trackId, std::size_t nbMaxCovers = 1) const;
std::vector<Image::Image> getFromRelease(Wt::Dbo::Session& session, Database::Release::id_type releaseId, std::size_t nbMaxCovers = 1) const;
std::vector<Image::Image> getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, std::size_t nbMaxCovers = 1) const;
std::vector<Image::Image> getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, std::size_t nbMaxCovers = 1) const;
private:
Grabber();
-863
View File
@@ -1,863 +0,0 @@
/*
* Copyright (C) 2013 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <boost/filesystem.hpp>
#include <boost/thread.hpp>
#include <boost/asio/placeholders.hpp>
#include "logger/Logger.hpp"
#include "utils/Utils.hpp"
#include "database/Types.hpp"
#include "Checksum.hpp"
#include "DatabaseUpdater.hpp"
#include "cover/CoverArtGrabber.hpp"
namespace {
boost::gregorian::date
getNextDay(const boost::gregorian::date& current)
{
boost::gregorian::day_iterator it(current);
return *(++it);
}
boost::gregorian::date
getNextMonday(const boost::gregorian::date& current)
{
boost::gregorian::day_iterator it(current);
++it;
// While it's not monday
while( it->day_of_week() != 1 )
++it;
return *(it);
}
boost::gregorian::date
getNextFirstOfMonth(const boost::gregorian::date& current)
{
boost::gregorian::day_iterator it(current);
++it;
// While it's not the 1st of the month
while( it->day() != 1 )
++it;
return (*it);
}
bool
isFileSupported(const boost::filesystem::path& file, const std::vector<boost::filesystem::path> extensions)
{
boost::filesystem::path fileExtension = file.extension();
for (auto& extension : extensions)
{
if (extension == fileExtension)
return true;
}
return false;
}
std::vector<boost::filesystem::path>
getRootDirectoriesByType(Wt::Dbo::Session& session, Database::MediaDirectory::Type type)
{
Wt::Dbo::Transaction transaction(session);
std::vector<boost::filesystem::path> res;
std::vector<Database::MediaDirectory::pointer> rootDirs = Database::MediaDirectory::getByType(session, type);
for (auto rootDir : rootDirs)
res.push_back(rootDir->getPath());
return res;
}
bool
isPathInParentPath(const boost::filesystem::path& path, const boost::filesystem::path& parentPath)
{
boost::filesystem::path curPath = path;
while (curPath.has_parent_path())
{
curPath = curPath.parent_path();
if (curPath == parentPath)
return true;
}
return false;
}
} // namespace
namespace DatabaseUpdater {
using namespace Database;
Updater::Updater(Wt::Dbo::SqlConnectionPool &connectionPool, MetaData::Parser& parser)
: _running(false),
_scheduleTimer(_ioService),
_db(connectionPool),
_metadataParser(parser)
{
_ioService.setThreadCount(1);
}
void
Updater::start(void)
{
_running = true;
// post some jobs in the io_service
processNextJob();
_ioService.start();
}
void
Updater::stop(void)
{
_running = false;
// TODO cancel all jobs (timer, ...)
_scheduleTimer.cancel();
_ioService.stop();
}
void
Updater::processNextJob(void)
{
Wt::Dbo::Transaction transaction(_db.getSession());
MediaDirectorySettings::pointer settings = MediaDirectorySettings::get(_db.getSession());
if (settings->getManualScanRequested()) {
LMS_LOG(DBUPDATER, INFO) << "Manual scan requested!";
scheduleScan( boost::posix_time::seconds(0) );
}
else
{
boost::posix_time::ptime now = boost::posix_time::second_clock::local_time();
boost::posix_time::time_duration startTime = settings->getUpdateStartTime();
boost::gregorian::date nextScanDate;
switch( settings->getUpdatePeriod() )
{
case Database::MediaDirectorySettings::Never:
// Nothing to do
break;
case Database::MediaDirectorySettings::Daily:
if (now.time_of_day() < startTime)
nextScanDate = now.date();
else
nextScanDate = getNextDay(now.date());
break;
case Database::MediaDirectorySettings::Weekly:
if (now.time_of_day() < startTime && now.date().day_of_week() == 1)
nextScanDate = now.date();
else
nextScanDate = getNextMonday(now.date());
break;
case Database::MediaDirectorySettings::Monthly:
if (now.time_of_day() < startTime && now.date().day() == 1)
nextScanDate = now.date();
else
nextScanDate = getNextFirstOfMonth(now.date());
break;
}
if (!nextScanDate.is_special())
scheduleScan( boost::posix_time::ptime (nextScanDate, settings->getUpdateStartTime() ) );
}
}
void
Updater::scheduleScan( boost::posix_time::time_duration duration)
{
LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan in " << duration;
_scheduleTimer.expires_from_now(duration);
_scheduleTimer.async_wait( boost::bind( &Updater::process, this, boost::asio::placeholders::error) );
}
void
Updater::scheduleScan( boost::posix_time::ptime time)
{
LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan at " << time;
_scheduleTimer.expires_at(time);
_scheduleTimer.async_wait( boost::bind( &Updater::process, this, boost::asio::placeholders::error) );
}
void
Updater::process(boost::system::error_code err)
{
if (!err)
{
updateFileExtensions();
Stats stats;
checkAudioFiles(stats);
checkVideoFiles(stats);
std::vector<RootDirectory> rootDirectories;
{
Wt::Dbo::Transaction transaction(_db.getSession());
for (MediaDirectory::pointer directory : MediaDirectory::getAll(_db.getSession()))
rootDirectories.push_back( RootDirectory( directory->getType(), directory->getPath() ));
}
for (RootDirectory rootDirectory : rootDirectories)
{
if (!_running)
break;
LMS_LOG(DBUPDATER, INFO) << "Processing root directory '" << rootDirectory.path << "'...";
processRootDirectory(rootDirectory, stats);
LMS_LOG(DBUPDATER, INFO) << "Processing root directory '" << rootDirectory.path << "' DONE";
}
if (_running)
checkDuplicatedAudioFiles(stats);
LMS_LOG(DBUPDATER, INFO) << "Scan complete. Scanned = " << stats.nbScanned << ", Skipped = " << stats.nbSkipped << ", Changes = " << stats.nbChanges() << " (added = " << stats.nbAdded << ", nbRemoved = " << stats.nbRemoved << ", nbModified = " << stats.nbModified << "), Scan errors = " << stats.nbScanErrors << ", Not imported = " << stats.nbNotImported;
// Update database stats
boost::posix_time::ptime now = boost::posix_time::second_clock::local_time();
{
Wt::Dbo::Transaction transaction(_db.getSession());
Database::MediaDirectorySettings::pointer settings = Database::MediaDirectorySettings::get(_db.getSession());
if (stats.nbChanges() > 0)
settings.modify()->setLastUpdate(now);
// Save the last scan only if it has been completed
if (_running)
settings.modify()->setLastScan(now);
// If the manual scan was required we can now set it to done
// Update only if the scan is complete!
if (settings->getManualScanRequested() && _running)
settings.modify()->setManualScanRequested(false);
}
if (_running)
processNextJob();
}
}
void
Updater::updateFileExtensions()
{
Wt::Dbo::Transaction transaction(_db.getSession());
_audioFileExtensions = MediaDirectorySettings::get(_db.getSession())->getAudioFileExtensions();
_videoFileExtensions = MediaDirectorySettings::get(_db.getSession())->getVideoFileExtensions();
}
Artist::pointer
Updater::getArtist( const boost::filesystem::path& file, const std::string& name, const std::string& mbid)
{
Artist::pointer artist;
// First try to get by MBID
if (!mbid.empty())
{
artist = Artist::getByMBID( _db.getSession(), mbid );
if (!artist)
artist = Artist::create( _db.getSession(), name, mbid);
return artist;
}
// Fall back on artist name (collisions may occur)
if (!name.empty())
{
for (Artist::pointer sameNamedArtist : Artist::getByName( _db.getSession(), name ))
{
if (sameNamedArtist->getMBID().empty())
{
artist = sameNamedArtist;
break;
}
}
// No Artist found with the same name and without MBID -> creating
if (!artist)
artist = Artist::create( _db.getSession(), name);
return artist;
}
return Artist::getNone( _db.getSession() );
}
Release::pointer
Updater::getRelease( const boost::filesystem::path& file, const std::string& name, const std::string& mbid)
{
Release::pointer release;
// First try to get by MBID
if (!mbid.empty())
{
release = Release::getByMBID( _db.getSession(), mbid );
if (!release)
release = Release::create( _db.getSession(), name, mbid);
return release;
}
// Fall back on release name (collisions may occur)
if (!name.empty())
{
for (Release::pointer sameNamedRelease : Release::getByName( _db.getSession(), name ))
{
if (sameNamedRelease->getMBID().empty())
{
release = sameNamedRelease;
break;
}
}
// No release found with the same name and without MBID -> creating
if (!release)
release = Release::create( _db.getSession(), name);
return release;
}
return Release::getNone( _db.getSession() );
}
std::vector<Genre::pointer>
Updater::getGenres( const std::list<std::string>& names)
{
std::vector< Genre::pointer > genres;
for (const std::string& name : names)
{
Genre::pointer genre ( Genre::getByName(_db.getSession(), name) );
if (!genre)
genre = Genre::create(_db.getSession(), name);
genres.push_back( genre );
}
if (genres.empty())
genres.push_back( Genre::getNone( _db.getSession() ));
return genres;
}
void
Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats)
{
boost::posix_time::ptime lastWriteTime (boost::posix_time::from_time_t( boost::filesystem::last_write_time( file ) ) );
// Skip file if last write is the same
{
Wt::Dbo::Transaction transaction(_db.getSession());
Wt::Dbo::ptr<Track> track = Track::getByPath(_db.getSession(), file);
if (track && track->getLastWriteTime() == lastWriteTime)
{
stats.nbSkipped++;
transaction.rollback();
return;
}
transaction.rollback();
}
MetaData::Items items;
if (!_metadataParser.parse(file, items))
{
stats.nbScanErrors++;
return;
}
stats.nbScanned++;
std::vector<unsigned char> checksum ;
computeCrc(file, checksum);
Wt::Dbo::Transaction transaction(_db.getSession());
Wt::Dbo::ptr<Track> track = Track::getByPath(_db.getSession(), file);
// We estimate this is a audio file if:
// - we found a least one audio stream
// - the duration is not null
if (items.find(MetaData::Type::AudioStreams) == items.end()
|| boost::any_cast<std::vector<MetaData::AudioStream> >(items[MetaData::Type::AudioStreams]).empty())
{
LMS_LOG(DBUPDATER, INFO) << "Skipped '" << file << "' (no audio stream found)";
// If Track exists here, delete it!
if (track)
{
track.remove();
stats.nbRemoved++;
}
stats.nbNotImported++;
return;
}
if (items.find(MetaData::Type::Duration) == items.end()
|| boost::any_cast<boost::posix_time::time_duration>(items[MetaData::Type::Duration]).total_seconds() <= 0)
{
LMS_LOG(DBUPDATER, INFO) << "Skipped '" << file << "' (no duration or duration <= 0)";
// If Track exists here, delete it!
if (track)
{
track.remove();
stats.nbRemoved++;
}
stats.nbNotImported++;
return;
}
// ***** Title
std::string title;
if (items.find(MetaData::Type::Title) != items.end())
{
title = boost::any_cast<std::string>(items[MetaData::Type::Title]);
}
else
{
// TODO parse file name guess track etc.
// For now juste use file name as title
title = file.filename().string();
}
// ***** Genres
std::vector< Genre::pointer > genres;
{
std::list<std::string> genreList;
if (items.find(MetaData::Type::Genres) != items.end())
genreList = boost::any_cast< std::list<std::string> > (items[MetaData::Type::Genres]);
genres = getGenres( genreList );
}
assert( !genres.empty() );
// ***** Artist
Artist::pointer artist;
{
std::string artistName;
std::string artistMusicBrainzID;
if (items.find(MetaData::Type::MusicBrainzArtistID) != items.end())
artistMusicBrainzID = boost::any_cast<std::string>(items[MetaData::Type::MusicBrainzArtistID] );
if (items.find(MetaData::Type::Artist) != items.end())
artistName = boost::any_cast<std::string>(items[MetaData::Type::Artist]);
artist = getArtist(file, artistName, artistMusicBrainzID);
}
assert(artist);
// ***** Release
Release::pointer release;
{
std::string releaseName;
std::string releaseMusicBrainzID;
if (items.find(MetaData::Type::MusicBrainzAlbumID) != items.end())
releaseMusicBrainzID = boost::any_cast<std::string>(items[MetaData::Type::MusicBrainzAlbumID] );
if (items.find(MetaData::Type::Album) != items.end())
releaseName = boost::any_cast<std::string>(items[MetaData::Type::Album]);
release = getRelease(file, releaseName, releaseMusicBrainzID);
}
assert(release);
// If file already exist, update data
// Otherwise, create it
if (!track)
{
// Create a new song
track = Track::create(_db.getSession(), file);
LMS_LOG(DBUPDATER, INFO) << "Adding '" << file << "'";
stats.nbAdded++;
}
else
{
LMS_LOG(DBUPDATER, INFO) << "Updating '" << file << "'";
stats.nbModified++;
}
assert(track);
track.modify()->setChecksum(checksum);
track.modify()->setArtist(artist);
track.modify()->setRelease(release);
track.modify()->setLastWriteTime(lastWriteTime);
track.modify()->setName(title);
track.modify()->setDuration( boost::any_cast<boost::posix_time::time_duration>(items[MetaData::Type::Duration]) );
track.modify()->setAddedTime( boost::posix_time::second_clock::local_time() );
{
std::string trackGenreList;
// Product genre list
for (Genre::pointer genre : genres)
{
if (!trackGenreList.empty())
trackGenreList += ", ";
trackGenreList += genre->getName();
}
track.modify()->setGenres( trackGenreList );
}
track.modify()->setGenres( genres );
if (items.find(MetaData::Type::TrackNumber) != items.end())
track.modify()->setTrackNumber( boost::any_cast<std::size_t>(items[MetaData::Type::TrackNumber]) );
if (items.find(MetaData::Type::TotalTrack) != items.end())
track.modify()->setTotalTrackNumber( boost::any_cast<std::size_t>(items[MetaData::Type::TotalTrack]) );
if (items.find(MetaData::Type::DiscNumber) != items.end())
track.modify()->setDiscNumber( boost::any_cast<std::size_t>(items[MetaData::Type::DiscNumber]) );
if (items.find(MetaData::Type::TotalDisc) != items.end())
track.modify()->setTotalDiscNumber( boost::any_cast<std::size_t>(items[MetaData::Type::TotalDisc]) );
if (items.find(MetaData::Type::Date) != items.end())
track.modify()->setDate( boost::any_cast<boost::posix_time::ptime>(items[MetaData::Type::Date]) );
if (items.find(MetaData::Type::OriginalDate) != items.end())
{
track.modify()->setOriginalDate( boost::any_cast<boost::posix_time::ptime>(items[MetaData::Type::OriginalDate]) );
// If a file has an OriginalDate but no date, set the date to ease filtering
if (items.find(MetaData::Type::Date) == items.end())
track.modify()->setDate( boost::any_cast<boost::posix_time::ptime>(items[MetaData::Type::OriginalDate]) );
}
if (items.find(MetaData::Type::MusicBrainzTrackID) != items.end())
{
track.modify()->setMBID( boost::any_cast<std::string>(items[MetaData::Type::MusicBrainzTrackID]) );
}
if (items.find(MetaData::Type::HasCover) != items.end())
{
bool hasCover = boost::any_cast<bool>(items[MetaData::Type::HasCover]);
track.modify()->setCoverType( hasCover ? Track::CoverType::Embedded : Track::CoverType::None );
}
transaction.commit();
}
void
Updater::processRootDirectory(RootDirectory rootDirectory, Stats& stats)
{
boost::system::error_code ec;
boost::filesystem::recursive_directory_iterator itPath(rootDirectory.path, ec);
boost::filesystem::recursive_directory_iterator itEnd;
while (!ec && itPath != itEnd)
{
boost::filesystem::path path = *itPath;
itPath.increment(ec);
if (!_running)
return;
if (boost::filesystem::is_regular(path))
{
switch( rootDirectory.type )
{
case Database::MediaDirectory::Audio:
if (isFileSupported(path, _audioFileExtensions))
processAudioFile(path, stats );
break;
case Database::MediaDirectory::Video:
if (isFileSupported(path, _videoFileExtensions))
processVideoFile(path, stats);
break;
}
}
}
}
bool
Updater::checkFile(const boost::filesystem::path& p, const std::vector<boost::filesystem::path>& rootDirs, const std::vector<boost::filesystem::path>& extensions)
{
try
{
bool status = true;
// For each track, make sure the the file still exists
// and still belongs to a root directory
if (!boost::filesystem::exists( p )
|| !boost::filesystem::is_regular( p ) )
{
LMS_LOG(DBUPDATER, INFO) << "Missing file '" << p << "'";
status = false;
}
else
{
bool foundRoot = false;
for (auto& rootDir : rootDirs)
{
if (isPathInParentPath(p, rootDir))
{
foundRoot = true;
break;
}
}
if (!foundRoot)
{
LMS_LOG(DBUPDATER, INFO) << "Out of root file '" << p << "'";
status = false;
}
else if (!isFileSupported(p, extensions))
{
LMS_LOG(DBUPDATER, INFO) << "File format no longer supported for '" << p << "'";
status = false;
}
}
return status;
}
catch (boost::filesystem::filesystem_error& e)
{
LMS_LOG(DBUPDATER, ERROR) << "Caught exception while checking file '" << p << "': " << e.what();
return false;
}
}
void
Updater::checkAudioFiles( Stats& stats )
{
LMS_LOG(DBUPDATER, INFO) << "Checking audio files...";
std::vector<boost::filesystem::path> trackPaths = Track::getAllPaths(_db.getSession());;
std::vector<boost::filesystem::path> rootDirs = getRootDirectoriesByType(_db.getSession(), Database::MediaDirectory::Audio);
LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks...";
for (auto& trackPath : trackPaths)
{
if (!_running)
return;
if (!checkFile(trackPath, rootDirs, _audioFileExtensions))
{
Wt::Dbo::Transaction transaction(_db.getSession());
Track::pointer track = Track::getByPath(_db.getSession(), trackPath);
if (track)
{
track.remove();
stats.nbRemoved++;
}
}
}
LMS_LOG(DBUPDATER, DEBUG) << "Checking Genres...";
{
Wt::Dbo::Transaction transaction(_db.getSession());
// Now process orphan Genre (no track)
auto genres = Genre::getAll(_db.getSession());
for (auto genre : genres)
{
if (genre->getTracks().size() == 0)
{
LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan genre '" << genre->getName() << "'";
genre.remove();
}
}
}
LMS_LOG(DBUPDATER, DEBUG) << "Checking artists...";
{
Wt::Dbo::Transaction transaction(_db.getSession());
auto artists = Artist::getAllOrphans(_db.getSession());
for (auto artist : artists)
{
LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan artist '" << artist->getName() << "'";
artist.remove();
}
}
LMS_LOG(DBUPDATER, DEBUG) << "Checking releases...";
{
Wt::Dbo::Transaction transaction(_db.getSession());
auto releases = Release::getAllOrphans(_db.getSession());
for (auto release : releases)
{
LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan release '" << release->getName() << "'";
release.remove();
}
}
LMS_LOG(DBUPDATER, INFO) << "Check audio files done!";
}
void
Updater::checkDuplicatedAudioFiles(Stats& stats)
{
LMS_LOG(DBUPDATER, INFO) << "Checking duplicated audio files";
Wt::Dbo::Transaction transaction(_db.getSession());
std::vector<Track::pointer> tracks = Database::Track::getMBIDDuplicates(_db.getSession());
for (Track::pointer track : tracks)
{
LMS_LOG(DBUPDATER, INFO) << "Found duplicated MBID [" << track->getMBID() << "], file: " << track->getPath() << " - " << track->getArtist()->getName() << " - " << track->getName();
}
tracks = Database::Track::getChecksumDuplicates(_db.getSession());
for (Track::pointer track : tracks)
{
LMS_LOG(DBUPDATER, INFO) << "Found duplicated checksum [" << bufferToString(track->getChecksum()) << "], file: " << track->getPath() << " - " << track->getArtist()->getName() << " - " << track->getName();
}
LMS_LOG(DBUPDATER, INFO) << "Checking duplicated audio files done!";
}
void
Updater::checkVideoFiles( Stats& stats )
{
std::vector<boost::filesystem::path> rootDirs = getRootDirectoriesByType(_db.getSession(), Database::MediaDirectory::Video);
std::vector<boost::filesystem::path> videoPaths = Video::getAllPaths(_db.getSession());
LMS_LOG(DBUPDATER, DEBUG) << "Checking videos...";
for (auto& videoPath : videoPaths)
{
if (!_running)
return;
if (!checkFile(videoPath, rootDirs, _videoFileExtensions))
{
Wt::Dbo::Transaction transaction(_db.getSession());
Video::pointer video = Video::getByPath(_db.getSession(), videoPath);
if (video)
{
video.remove();
stats.nbRemoved++;
}
}
}
LMS_LOG(DBUPDATER, DEBUG) << "Check video files done!";
}
void
Updater::processVideoFile( const boost::filesystem::path& file, Stats& stats)
{
// Check last update time
boost::posix_time::ptime lastWriteTime (boost::posix_time::from_time_t( boost::filesystem::last_write_time( file ) ) );
Wt::Dbo::Transaction transaction(_db.getSession());
// Skip file if last write is the same
Wt::Dbo::ptr<Video> video = Video::getByPath(_db.getSession(), file);
if (video && video->getLastWriteTime() == lastWriteTime)
return;
MetaData::Items items;
if (!_metadataParser.parse(file, items))
return;
// We estimate this is a video if:
// - we found a least one video stream
// - the duration is not null
if (items.find(MetaData::Type::VideoStreams) == items.end()
|| boost::any_cast<std::vector<MetaData::VideoStream> >(items[MetaData::Type::VideoStreams]).empty())
{
LMS_LOG(DBUPDATER, ERROR) << "Skipped '" << file << "' (no video stream found)";
// If the video exists here, delete it!
if (video) {
video.remove();
stats.nbRemoved++;
}
return;
}
if (items.find(MetaData::Type::Duration) == items.end()
|| boost::any_cast<boost::posix_time::time_duration>(items[MetaData::Type::Duration]).total_seconds() == 0)
{
LMS_LOG(DBUPDATER, ERROR) << "Skipped '" << file << "' (no duration or duration 0)";
// If Track exists here, delete it!
if (video) {
video.remove();
stats.nbRemoved++;
}
return;
}
// If video already exist, update data
// Otherwise, create it
// Today we are very aggressive, but we could also guess names from path, etc.
if (!video)
{
video = Video::create(_db.getSession(), file);
LMS_LOG(DBUPDATER, DEBUG) << "Adding '" << file << "'";
stats.nbAdded++;
}
else
{
LMS_LOG(DBUPDATER, DEBUG) << "Updating '" << file << "'";
stats.nbModified++;
}
assert(video);
video.modify()->setName( file.filename().string() );
video.modify()->setDuration( boost::any_cast<boost::posix_time::time_duration>(items[MetaData::Type::Duration]) );
video.modify()->setLastWriteTime(lastWriteTime);
transaction.commit();
}
} // namespace DatabaseUpdater
-114
View File
@@ -1,114 +0,0 @@
/*
* Copyright (C) 2013 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef DB_UPDATER_HPP
#define DB_UPDATER_HPP
#include <boost/asio/deadline_timer.hpp>
#include <Wt/WIOService>
#include "metadata/MetaData.hpp"
#include "database/DatabaseHandler.hpp"
namespace DatabaseUpdater {
class Updater
{
public:
Updater(Wt::Dbo::SqlConnectionPool& connectionPool, MetaData::Parser& parser);
void setAudioExtensions(const std::vector<std::string>& extensions);
void setVideoExtensions(const std::vector<std::string>& extensions);
void start();
void stop();
private:
struct Stats
{
std::size_t nbSkipped = 0; // no change since last scan
std::size_t nbScanned = 0;
std::size_t nbScanErrors = 0; // cannot scan file
std::size_t nbNotImported = 0; // Not imported (criteria not filled)
std::size_t nbAdded = 0;
std::size_t nbRemoved = 0;
std::size_t nbModified = 0;
std::size_t nbChanges() const { return nbAdded + nbRemoved + nbModified;}
};
struct RootDirectory
{
Database::MediaDirectory::Type type;
boost::filesystem::path path;
RootDirectory(Database::MediaDirectory::Type t, boost::filesystem::path p) : type(t), path(p) {}
};
// Job handling
void processNextJob();
void scheduleScan(boost::posix_time::time_duration duration);
void scheduleScan(boost::posix_time::ptime time);
// Update database (scheduled callback)
void process(boost::system::error_code ec);
// Check if a file exists and is still in a root directory
static bool checkFile(const boost::filesystem::path& p,
const std::vector<boost::filesystem::path>& rootDirectories,
const std::vector<boost::filesystem::path>& extensions);
void processRootDirectory( RootDirectory rootDirectory, Stats& stats);
// Helpers
Database::Artist::pointer getArtist( const boost::filesystem::path& file, const std::string& name, const std::string& MBID);
Database::Release::pointer getRelease( const boost::filesystem::path& file, const std::string& name, const std::string& MBID);
std::vector<Database::Genre::pointer> getGenres( const std::list<std::string>& names);
void updateFileExtensions();
// Audio
void checkAudioFiles( Stats& stats );
void checkDuplicatedAudioFiles( Stats& stats );
void processAudioFile( const boost::filesystem::path& file, Stats& stats);
// Video
void checkVideoFiles( Stats& stats );
void processVideoFile( const boost::filesystem::path& file, Stats& stats);
bool _running;
Wt::WIOService _ioService;
boost::asio::deadline_timer _scheduleTimer;
Database::Handler _db;
std::vector<boost::filesystem::path> _audioFileExtensions;
std::vector<boost::filesystem::path> _videoFileExtensions;
MetaData::Parser& _metadataParser;
}; // class Updater
} // DatabaseUpdater
#endif
+143 -65
View File
@@ -16,11 +16,16 @@
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Artist.hpp"
#include "Types.hpp"
#include <Wt/Dbo/WtSqlTraits.h>
#include "utils/Logger.hpp"
#include "Cluster.hpp"
#include "Release.hpp"
#include "SqlQuery.hpp"
#include "logger/Logger.hpp"
#include "Track.hpp"
namespace Database
{
@@ -46,7 +51,7 @@ Artist::getByMBID(Wt::Dbo::Session& session, const std::string& mbid)
}
Artist::pointer
Artist::getById(Wt::Dbo::Session& session, Artist::id_type id)
Artist::getById(Wt::Dbo::Session& session, IdType id)
{
return session.find<Artist>().where("id = ?").bind(id);
}
@@ -54,46 +59,77 @@ Artist::getById(Wt::Dbo::Session& session, Artist::id_type id)
Artist::pointer
Artist::create(Wt::Dbo::Session& session, const std::string& name, const std::string& MBID)
{
return session.add(new Artist(name, MBID));
}
Artist::pointer
Artist::getNone(Wt::Dbo::Session& session)
{
std::vector<pointer> res = getByName(session, "<None>");
if (res.empty())
return create(session, "<None>");
return res.front();
return session.add(std::make_unique<Artist>(name, MBID));
}
std::vector<Artist::pointer>
Artist::getAll(Wt::Dbo::Session& session, int offset, int size)
{
Wt::Dbo::collection<pointer> res = session.find<Artist>().offset(offset).limit(size);
Wt::Dbo::collection<pointer> res = session.find<Artist>().orderBy("LOWER(name)").offset(offset).limit(size);
return std::vector<pointer>(res.begin(), res.end());
}
std::vector<Artist::pointer>
Artist::getAllOrphans(Wt::Dbo::Session& session)
{
Wt::Dbo::collection<Artist::pointer> res = session.query< Wt::Dbo::ptr<Artist> >("select a from artist a LEFT OUTER JOIN Track t ON a.id = t.artist_id WHERE t.id IS NULL");
Wt::Dbo::collection<Artist::pointer> res = session.query< Wt::Dbo::ptr<Artist> >("SELECT DISTINCT a FROM artist a LEFT OUTER JOIN Track t ON a.id = t.artist_id WHERE t.id IS NULL");
return std::vector<pointer>(res.begin(), res.end());
}
std::vector<Artist::pointer>
Artist::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, int size)
static
Wt::Dbo::Query<Artist::pointer>
getQuery(Wt::Dbo::Session& session,
const std::set<IdType>& clusterIds,
const std::vector<std::string>& keywords)
{
Wt::Dbo::collection<Artist::pointer> res = getQuery(session, filter).limit(size).offset(offset);
WhereClause where;
return std::vector<pointer>(res.begin(), res.end());
std::ostringstream oss;
oss << "SELECT DISTINCT a FROM artist a";
for (auto keyword : keywords)
where.And(WhereClause("a.name LIKE ?")).bind("%%" + keyword + "%%");
if (!clusterIds.empty())
{
oss << " INNER JOIN track t ON t.artist_id = a.id INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
WhereClause clusterClause;
for (auto id : clusterIds)
clusterClause.Or(WhereClause("c.id = ?")).bind(std::to_string(id));
where.And(clusterClause);
}
oss << " " << where.get();
if (!clusterIds.empty())
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size();
oss << " ORDER BY a.name";
Wt::Dbo::Query<Artist::pointer> query = session.query<Artist::pointer>( oss.str() );
for (const std::string& bindArg : where.getBindArgs())
{
query.bind(bindArg);
}
return query;
}
std::vector<Artist::pointer>
Artist::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, int size, bool& moreResults)
Artist::getByFilter(Wt::Dbo::Session& session,
const std::set<IdType>& clusters,
const std::vector<std::string> keywords,
int offset, int size, bool& moreResults)
{
auto res = getByFilter(session, filter, offset, size + 1);
Wt::Dbo::collection<Artist::pointer> collection = getQuery(session, clusters, keywords)
.limit(size != -1 ? size + 1 : -1)
.offset(offset);
auto res = std::vector<pointer>(collection.begin(), collection.end());
if (size != -1 && res.size() == static_cast<std::size_t>(size) + 1)
{
@@ -106,63 +142,105 @@ Artist::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset,
return res;
}
std::vector<Artist::pointer>
Artist::getLastAdded(Wt::Dbo::Session& session, Wt::WDateTime after, int limit)
{
Wt::Dbo::collection<Artist::pointer> res = session.query<Artist::pointer>("SELECT a from artist a INNER JOIN track t ON a.id = t.artist_id")
.where("t.file_added > ?").bind(after)
.groupBy("a.id")
.orderBy("t.file_added DESC")
.limit(limit);
return std::vector<pointer>(res.begin(), res.end());
}
std::vector<Wt::Dbo::ptr<Release> >
Artist::getReleases() const
std::vector<Wt::Dbo::ptr<Release>>
Artist::getReleases(const std::set<IdType>& clusterIds) const
{
assert(self());
assert(self()->id() != Wt::Dbo::dbo_traits<Artist>::invalidId() );
assert(IdIsValid(self()->id()));
assert(session());
Wt::Dbo::collection< Wt::Dbo::ptr<Release> > res = session()->query<Wt::Dbo::ptr<Release> >("SELECT r FROM release r INNER JOIN artist a ON t.artist_id = a.id INNER JOIN track t ON t.release_id = r.id").where("a.id = ?").bind(id());
WhereClause where;
std::ostringstream oss;
oss << "SELECT DISTINCT r FROM release r INNER JOIN artist a ON t.artist_id = a.id INNER JOIN track t ON t.release_id = r.id";
if (!clusterIds.empty())
{
oss << " INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
WhereClause clusterClause;
for (auto id : clusterIds)
clusterClause.Or(WhereClause("c.id = ?")).bind(std::to_string(id));
where.And(clusterClause);
}
where.And(WhereClause("a.id = ?")).bind(std::to_string(id()));
oss << " " << where.get();
if (!clusterIds.empty())
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size();
oss << " ORDER BY t.year,r.name";
Wt::Dbo::Query<Release::pointer> query = session()->query<Release::pointer>( oss.str() );
for (const std::string& bindArg : where.getBindArgs())
{
query.bind(bindArg);
}
Wt::Dbo::collection< Wt::Dbo::ptr<Release> > res = query;
return std::vector< Wt::Dbo::ptr<Release> > (res.begin(), res.end());
}
Wt::Dbo::Query<Artist::pointer>
Artist::getQuery(Wt::Dbo::Session& session, SearchFilter filter)
std::vector<std::vector<Wt::Dbo::ptr<Cluster>>>
Artist::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, std::size_t size) const
{
SqlQuery sqlQuery = generatePartialQuery(filter);
assert(self());
assert(IdIsValid(self()->id()));
assert(session());
Wt::Dbo::Query<pointer> query
= session.query<pointer>( "SELECT a FROM artist a INNER JOIN track t ON t.artist_id = a.id INNER JOIN release r ON r.id = t.release_id INNER JOIN genre g ON g.id = t_g.genre_id INNER JOIN track_genre t_g ON t_g.track_id = t.id " + sqlQuery.where().get()).groupBy("a.id").orderBy("a.name");
WhereClause where;
for (const std::string& bindArg : sqlQuery.where().getBindArgs())
query.bind(bindArg);
std::ostringstream oss;
oss << "SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN cluster_type c_type ON c.cluster_type_id = c_type.id INNER JOIN artist a ON t.artist_id = a.id";
return query;
}
Wt::Dbo::Query<Artist::UIQueryResult>
Artist::getUIQuery(Wt::Dbo::Session& session, SearchFilter filter)
{
SqlQuery sqlQuery = generatePartialQuery(filter);
Wt::Dbo::Query<UIQueryResult> query
= session.query<UIQueryResult>( "SELECT a.id, a.name, COUNT(DISTINCT r.id), COUNT(DISTINCT t.id) FROM artist a INNER JOIN track t ON t.artist_id = a.id INNER JOIN release r ON r.id = t.release_id INNER JOIN genre g ON g.id = t_g.genre_id INNER JOIN track_genre t_g ON t_g.track_id = t.id " + sqlQuery.where().get()).groupBy("a.id").orderBy("a.name");
for (const std::string& bindArg : sqlQuery.where().getBindArgs())
query.bind(bindArg);
return query;
}
void
Artist::updateUIQueryModel(Wt::Dbo::Session& session, Wt::Dbo::QueryModel<UIQueryResult>& model, SearchFilter filter, const std::vector<Wt::WString>& columnNames)
{
Wt::Dbo::Query<UIQueryResult> query = getUIQuery(session, filter);
model.setQuery(query, columnNames.empty() ? true : false);
// TODO do something better
if (columnNames.size() == 3)
where.And(WhereClause("a.id = ?")).bind(std::to_string(self()->id()));
{
model.addColumn( "a.name", columnNames.at(0));
model.addColumn( "COUNT(DISTINCT r.id)", columnNames.at(1) );
model.addColumn( "COUNT(DISTINCT t.id)", columnNames.at(2) );
}
}
WhereClause clusterClause;
for (auto clusterType : clusterTypes)
clusterClause.Or(WhereClause("c_type.id = ?")).bind(std::to_string(clusterType.id()));
where.And(clusterClause);
}
oss << " " << where.get();
oss << "GROUP BY c.id ORDER BY COUNT(c.id) DESC";
Wt::Dbo::Query<Cluster::pointer> query = session()->query<Cluster::pointer>( oss.str() );
for (const std::string& bindArg : where.getBindArgs())
query.bind(bindArg);
Wt::Dbo::collection<Cluster::pointer> queryRes = query;
std::map<IdType, std::vector<Cluster::pointer>> clusters;
for (auto cluster : queryRes)
{
if (clusters[cluster->getType().id()].size() < size)
clusters[cluster->getType().id()].push_back(cluster);
}
std::vector<std::vector<Cluster::pointer>> res;
for (auto cluster_list : clusters)
res.push_back(cluster_list.second);
return res;
}
} // namespace Database
+23 -25
View File
@@ -17,22 +17,22 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _DB_ARTIST_HPP_
#define _DB_ARTIST_HPP_
#pragma once
#include <string>
#include <vector>
#include <Wt/Dbo/Dbo>
#include <Wt/Dbo/QueryModel>
#include <Wt/WDateTime.h>
#include <Wt/Dbo/Dbo.h>
#include "SearchFilter.hpp"
#include "Types.hpp"
namespace Database
{
class Track;
class Genre;
class Cluster;
class ClusterType;
class Release;
class Artist : public Wt::Dbo::Dbo<Artist>
@@ -40,41 +40,42 @@ class Artist : public Wt::Dbo::Dbo<Artist>
public:
typedef Wt::Dbo::ptr<Artist> pointer;
typedef Wt::Dbo::dbo_traits<Artist>::IdType id_type;
Artist() {}
Artist(const std::string& name, const std::string& MBID = "");
// Accessors
static pointer getByMBID(Wt::Dbo::Session& session, const std::string& MBID);
static pointer getById(Wt::Dbo::Session& session, id_type id);
static pointer getNone(Wt::Dbo::Session& session); // Special entry
static pointer getById(Wt::Dbo::Session& session, IdType id);
static std::vector<pointer> getByName(Wt::Dbo::Session& session, const std::string& name);
static std::vector<pointer> getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset = -1, int size = -1);
static std::vector<pointer> getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, int size, bool& moreExpected);
static std::vector<pointer> getByFilter(Wt::Dbo::Session& session,
const std::set<IdType>& clusters, // at least one track that belongs to these clusters
const std::vector<std::string> keywords, // name must match all of these keywords
int offset,
int size,
bool& moreExpected);
static std::vector<pointer> getAll(Wt::Dbo::Session& session, int offset = -1, int size = -1);
static std::vector<pointer> getAllOrphans(Wt::Dbo::Session& session);
static std::vector<pointer> getAllOrphans(Wt::Dbo::Session& session); // No track related
static std::vector<pointer> getLastAdded(Wt::Dbo::Session& session, Wt::WDateTime after, int size = 1);
// Accessors
std::string getName(void) const { return _name; }
std::string getMBID(void) const { return _MBID; }
// Get the releases that have at least one track for this artist
std::vector<Wt::Dbo::ptr<Release> > getReleases() const;
// Get the releases that have at least one track for this artist that belongs to optional cluster filters
std::vector<Wt::Dbo::ptr<Release>> getReleases(const std::set<IdType>& clusterIds = std::set<IdType>()) const;
// Get the cluster of the tracks made by this artist
// Each clusters are grouped by cluster type, sorted by the number of occurence
// size is the max number of cluster per cluster type
std::vector<std::vector<Wt::Dbo::ptr<Cluster>>> getClusterGroups(std::vector<Wt::Dbo::ptr<ClusterType>> clusterTypes, std::size_t size) const;
void setMBID(std::string mbid) { _MBID = mbid; }
// Create
static pointer create(Wt::Dbo::Session& session, const std::string& name, const std::string& MBID = "");
// MVC models for the user interface
// ID, Artist name, albums, tracks
typedef boost::tuple<id_type, std::string, int, int> UIQueryResult;
static Wt::Dbo::Query<UIQueryResult> getUIQuery(Wt::Dbo::Session& session, SearchFilter filter);
static void updateUIQueryModel(Wt::Dbo::Session& session, Wt::Dbo::QueryModel<UIQueryResult>& model, SearchFilter filter, const std::vector<Wt::WString>& columnNames = std::vector<Wt::WString>());
bool isNone(void) const;
template<class Action>
void persist(Action& a)
@@ -87,16 +88,13 @@ class Artist : public Wt::Dbo::Dbo<Artist>
private:
static Wt::Dbo::Query<pointer> getQuery(Wt::Dbo::Session& session, SearchFilter filter);
static const std::size_t _maxNameLength = 128;
std::string _name;
std::string _MBID; // Musicbrainz Identifier
Wt::Dbo::collection< Wt::Dbo::ptr<Track> > _tracks; // Tracks of this artist
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks; // Tracks of this artist
};
} // namespace Database
#endif
+153
View File
@@ -0,0 +1,153 @@
/*
* Copyright (C) 2013-2016 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Cluster.hpp"
#include "Artist.hpp"
#include "Release.hpp"
#include "ScanSettings.hpp"
#include "SqlQuery.hpp"
#include "Track.hpp"
namespace Database {
Cluster::Cluster()
{
}
Cluster::Cluster(Wt::Dbo::ptr<ClusterType> type, std::string name)
: _name(std::string(name, 0, _maxNameLength)),
_clusterType(type)
{
}
Cluster::pointer
Cluster::create(Wt::Dbo::Session& session, Wt::Dbo::ptr<ClusterType> type, std::string name)
{
return session.add(std::make_unique<Cluster>(type, name));
}
std::vector<Cluster::pointer>
Cluster::getAll(Wt::Dbo::Session& session)
{
Wt::Dbo::collection<Cluster::pointer> res = session.find<Cluster>();
return std::vector<Cluster::pointer>(res.begin(), res.end());
}
Cluster::pointer
Cluster::getById(Wt::Dbo::Session& session, IdType id)
{
return session.find<Cluster>().where("id = ?").bind(id);
}
void
Cluster::addTrack(Wt::Dbo::ptr<Track> track)
{
_tracks.insert(track);
}
std::vector<Wt::Dbo::ptr<Track>>
Cluster::getTracks(int offset, int limit) const
{
Wt::Dbo::collection<Track::pointer> res = session()->query<Track::pointer>("select t from track t INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
.where("c.id = ?").bind(self()->id())
.offset(offset)
.limit(limit);
return std::vector<Wt::Dbo::ptr<Track>>(res.begin(), res.end());
}
std::set<IdType>
Cluster::getTrackIds() const
{
assert(session());
assert(IdIsValid(self()->id()));
Wt::Dbo::collection<IdType> res = session()->query<IdType>("SELECT t_c.track_id from track_cluster t_c INNER JOIN cluster c ON c.id = t_c.cluster_id")
.where("c.id = ?").bind(self()->id());
return std::set<IdType>(res.begin(), res.end());
}
ClusterType::ClusterType(std::string name)
: _name(name)
{
}
std::vector<ClusterType::pointer>
ClusterType::getAllOrphans(Wt::Dbo::Session& session)
{
Wt::Dbo::collection<pointer> res = session.query<Wt::Dbo::ptr<ClusterType>>("select c_t from cluster_type c_t LEFT OUTER JOIN cluster c ON c_t.id = c.cluster_type_id WHERE c.id IS NULL");
return std::vector<pointer>(res.begin(), res.end());
}
ClusterType::pointer
ClusterType::getByName(Wt::Dbo::Session& session, std::string name)
{
return session.find<ClusterType>().where("name = ?").bind(name);
}
std::vector<ClusterType::pointer>
ClusterType::getAll(Wt::Dbo::Session& session)
{
Wt::Dbo::collection<pointer> res = session.find<ClusterType>();
return std::vector<pointer>(res.begin(), res.end());
}
ClusterType::pointer
ClusterType::create(Wt::Dbo::Session& session, std::string name)
{
return session.add(std::make_unique<ClusterType>(name));
}
Cluster::pointer
ClusterType::getCluster(std::string name) const
{
assert(self());
assert(IdIsValid(self()->id()));
assert(session());
return session()->find<Cluster>()
.where("name = ?").bind(name)
.where("cluster_type_id = ?").bind(self()->id());
}
std::vector<Cluster::pointer>
ClusterType::getClusters() const
{
assert(self());
assert(IdIsValid(self()->id()));
assert(session());
Wt::Dbo::collection<Cluster::pointer> res = session()->find<Cluster>()
.where("cluster_type_id = ?").bind(self()->id())
.orderBy("name");
return std::vector<Cluster::pointer>(res.begin(), res.end());
}
} // namespace Database
+120
View File
@@ -0,0 +1,120 @@
/*
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <string>
#include <vector>
#include <Wt/Dbo/Dbo.h>
#include <Wt/WDateTime.h>
#include "Types.hpp"
namespace Database {
class Track;
class ClusterType;
class ScanSettings;
class Cluster : public Wt::Dbo::Dbo<Cluster>
{
public:
typedef Wt::Dbo::ptr<Cluster> pointer;
Cluster();
Cluster(Wt::Dbo::ptr<ClusterType> type, std::string name);
// Find utility
static std::vector<pointer> getAll(Wt::Dbo::Session& session);
static pointer getById(Wt::Dbo::Session& session, IdType id);
// Create utility
static pointer create(Wt::Dbo::Session& session, Wt::Dbo::ptr<ClusterType> type, std::string name);
// Accessors
const std::string& getName(void) const { return _name; }
Wt::Dbo::ptr<ClusterType> getType() const { return _clusterType; }
std::size_t getCount() const { return _tracks.size(); }
std::vector<Wt::Dbo::ptr<Track>> getTracks(int offset, int limit) const;
std::set<IdType> getTrackIds() const;
void addTrack(Wt::Dbo::ptr<Track> track);
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::belongsTo(a, _clusterType, "cluster_type", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
}
private:
static const std::size_t _maxNameLength = 128;
std::string _name;
Wt::Dbo::ptr<ClusterType> _clusterType;
Wt::Dbo::collection< Wt::Dbo::ptr<Track> > _tracks;
};
class ClusterType : public Wt::Dbo::Dbo<ClusterType>
{
public:
using pointer = Wt::Dbo::ptr<ClusterType>;
ClusterType() {}
ClusterType(std::string name);
static std::vector<pointer> getAllOrphans(Wt::Dbo::Session& session);
static pointer getByName(Wt::Dbo::Session& session, std::string name);
static std::vector<pointer> getAll(Wt::Dbo::Session& session);
static pointer create(Wt::Dbo::Session& session, std::string name);
static void remove(Wt::Dbo::Session& session, std::string name);
// Accessors
const std::string& getName(void) const { return _name; }
std::vector<Cluster::pointer> getClusters() const;
Cluster::pointer getCluster(std::string name) const;
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToOne, "cluster_type");
Wt::Dbo::belongsTo(a, _scanSettings, "scan_settings", Wt::Dbo::OnDeleteCascade);
}
private:
static const std::size_t _maxNameLength = 128;
std::string _name;
Wt::Dbo::collection< Wt::Dbo::ptr<Cluster> > _clusters;
Wt::Dbo::ptr<ScanSettings> _scanSettings;
};
} // namespace Database
+86 -54
View File
@@ -17,21 +17,29 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <Wt/Dbo/FixedSqlConnectionPool>
#include <Wt/Dbo/backend/Sqlite3>
#include <Wt/Auth/Dbo/AuthInfo>
#include <Wt/Auth/Dbo/UserDatabase>
#include <Wt/Auth/AuthService>
#include <Wt/Auth/HashFunction>
#include <Wt/Auth/PasswordService>
#include <Wt/Auth/PasswordStrengthValidator>
#include <Wt/Auth/PasswordVerifier>
#include "logger/Logger.hpp"
#include "DatabaseHandler.hpp"
#include <Wt/Dbo/FixedSqlConnectionPool.h>
#include <Wt/Dbo/backend/Sqlite3.h>
#include <Wt/Auth/Dbo/AuthInfo.h>
#include <Wt/Auth/Dbo/UserDatabase.h>
#include <Wt/Auth/AuthService.h>
#include <Wt/Auth/HashFunction.h>
#include <Wt/Auth/Identity.h>
#include <Wt/Auth/PasswordService.h>
#include <Wt/Auth/PasswordStrengthValidator.h>
#include <Wt/Auth/PasswordVerifier.h>
#include "utils/Logger.hpp"
#include "Artist.hpp"
#include "Cluster.hpp"
#include "TrackList.hpp"
#include "Release.hpp"
#include "ScanSettings.hpp"
#include "Track.hpp"
namespace Database {
@@ -44,22 +52,31 @@ namespace {
void
Handler::configureAuth(void)
{
authService.setEmailVerificationEnabled(true);
authService.setEmailVerificationEnabled(false);
authService.setAuthTokensEnabled(true, "lmsauth");
authService.setIdentityPolicy(Wt::Auth::IdentityPolicy::LoginName);
authService.setRandomTokenLength(32);
Wt::Auth::PasswordVerifier *verifier = new Wt::Auth::PasswordVerifier();
verifier->addHashFunction(new Wt::Auth::BCryptHashFunction(8));
passwordService.setVerifier(verifier);
#if WT_VERSION < 0X04000300
authService.setTokenHashFunction(new Wt::Auth::BCryptHashFunction(8));
#else
authService.setTokenHashFunction(std::make_unique<Wt::Auth::BCryptHashFunction>(8));
#endif
auto verifier = std::make_unique<Wt::Auth::PasswordVerifier>();
verifier->addHashFunction(std::make_unique<Wt::Auth::BCryptHashFunction>(8));
passwordService.setVerifier(std::move(verifier));
passwordService.setAttemptThrottlingEnabled(true);
Wt::Auth::PasswordStrengthValidator* strengthValidator = new Wt::Auth::PasswordStrengthValidator();
auto strengthValidator = std::make_unique<Wt::Auth::PasswordStrengthValidator>();
// Reduce some constraints...
strengthValidator->setMinimumLength( Wt::Auth::PasswordStrengthValidator::PassPhrase, 4);
strengthValidator->setMinimumLength( Wt::Auth::PasswordStrengthValidator::OneCharClass, 4);
strengthValidator->setMinimumLength( Wt::Auth::PasswordStrengthValidator::TwoCharClass, 4);
strengthValidator->setMinimumLength( Wt::Auth::PasswordStrengthValidator::ThreeCharClass, 4 );
strengthValidator->setMinimumLength( Wt::Auth::PasswordStrengthValidator::FourCharClass, 4 );
strengthValidator->setMinimumLength( Wt::Auth::PasswordStrengthType::PassPhrase, 4);
strengthValidator->setMinimumLength( Wt::Auth::PasswordStrengthType::OneCharClass, 4);
strengthValidator->setMinimumLength( Wt::Auth::PasswordStrengthType::TwoCharClass, 4);
strengthValidator->setMinimumLength( Wt::Auth::PasswordStrengthType::ThreeCharClass, 4 );
strengthValidator->setMinimumLength( Wt::Auth::PasswordStrengthType::FourCharClass, 4 );
passwordService.setStrengthValidator(strengthValidator);
passwordService.setStrengthValidator(std::move(strengthValidator));
}
const Wt::Auth::AuthService&
@@ -79,39 +96,45 @@ Handler::Handler(Wt::Dbo::SqlConnectionPool& connectionPool)
{
_session.setConnectionPool(connectionPool);
_session.mapClass<Artist>("artist");
_session.mapClass<Cluster>("cluster");
_session.mapClass<ClusterType>("cluster_type");
_session.mapClass<TrackList>("tracklist");
_session.mapClass<TrackListEntry>("tracklist_entry");
_session.mapClass<Release>("release");
_session.mapClass<Track>("track");
_session.mapClass<Database::Artist>("artist");
_session.mapClass<Database::Genre>("genre");
_session.mapClass<Database::Track>("track");
_session.mapClass<Database::Playlist>("playlist");
_session.mapClass<Database::PlaylistEntry>("playlist_entry");
_session.mapClass<Database::Release>("release");
_session.mapClass<Database::Video>("video");
_session.mapClass<Database::MediaDirectory>("media_directory");
_session.mapClass<Database::MediaDirectorySettings>("media_directory_settings");
_session.mapClass<ScanSettings>("scan_settings");
_session.mapClass<Database::User>("user");
_session.mapClass<Database::AuthInfo>("auth_info");
_session.mapClass<Database::AuthInfo::AuthIdentityType>("auth_identity");
_session.mapClass<Database::AuthInfo::AuthTokenType>("auth_token");
_session.mapClass<AuthInfo>("auth_info");
_session.mapClass<AuthInfo::AuthIdentityType>("auth_identity");
_session.mapClass<AuthInfo::AuthTokenType>("auth_token");
_session.mapClass<User>("user");
try {
Wt::Dbo::Transaction transaction(_session);
_session.createTables();
_session.execute("CREATE INDEX artist_name_idx ON artist(name)");
_session.execute("CREATE INDEX genre_name_idx ON genre(name)");
_session.execute("CREATE INDEX release_name_idx ON release(name)");
_session.execute("CREATE INDEX track_name_idx ON track(name)");
LMS_LOG(DB, INFO) << "Tables created";
}
catch(std::exception& e) {
catch (Wt::Dbo::Exception& e)
{
LMS_LOG(DB, ERROR) << "Cannot create tables: " << e.what();
}
{
Wt::Dbo::Transaction transaction(_session);
_session.execute("PRAGMA journal_mode=WAL");
// Indexes
_session.execute("CREATE INDEX IF NOT EXISTS track_path_idx ON track(file_path)");
_session.execute("CREATE INDEX IF NOT EXISTS artist_name_idx ON artist(name)");
_session.execute("CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)");
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_idx ON track(artist_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_release_idx ON track(release_id)");
_session.execute("CREATE INDEX IF NOT EXISTS cluster_name_idx ON cluster(name)");
_session.execute("CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)");
_session.execute("CREATE INDEX IF NOT EXISTS tracklist_name ON tracklist(name)");
}
_users = new UserDatabase(_session);
@@ -140,7 +163,6 @@ Handler::getCurrentUser()
User::pointer
Handler::getUser(const Wt::Auth::User& authUser)
{
if (!authUser.isValid()) {
LMS_LOG(DB, ERROR) << "Handler::getUser: invalid authUser";
return User::pointer();
@@ -148,27 +170,37 @@ Handler::getUser(const Wt::Auth::User& authUser)
Wt::Dbo::ptr<AuthInfo> authInfo = _users->find(authUser);
User::pointer user = authInfo->user();
return authInfo->user();
}
if (!user) {
user = _session.add(new User());
authInfo.modify()->setUser(user);
User::pointer
Handler::createUser(const Wt::Auth::User& authUser)
{
if (!authUser.isValid()) {
LMS_LOG(DB, ERROR) << "Handler::getUser: invalid authUser";
return User::pointer();
}
User::pointer user = _session.add(std::make_unique<User>());
Wt::Dbo::ptr<AuthInfo> authInfo = _users->find(authUser);
authInfo.modify()->setUser(user);
return user;
}
Wt::Dbo::SqlConnectionPool*
std::unique_ptr<Wt::Dbo::SqlConnectionPool>
Handler::createConnectionPool(boost::filesystem::path p)
{
LMS_LOG(DB, INFO) << "Creating connection pool on file " << p;
Wt::Dbo::backend::Sqlite3 *connection = new Wt::Dbo::backend::Sqlite3(p.string());
LMS_LOG(DB, INFO) << "Creating connection pool on file " << p.string();
auto connection = std::make_unique<Wt::Dbo::backend::Sqlite3>(p.string());
connection->executeSql("pragma journal_mode=WAL");
connection->setProperty("show-queries", "true");
// connection->setProperty("show-queries", "true");
return new Wt::Dbo::FixedSqlConnectionPool(connection, 1);
auto pool = std::make_unique<Wt::Dbo::FixedSqlConnectionPool>(std::move(connection), 1);
pool->setTimeout(std::chrono::seconds(10));
return pool;
}
+12 -12
View File
@@ -17,23 +17,23 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef DATABASE_HANDLER_HPP
#define DATABASE_HANDLER_HPP
#pragma once
#include <boost/filesystem.hpp>
#include <memory>
#include <Wt/Dbo/Dbo>
#include <Wt/Dbo/SqlConnectionPool>
#include <Wt/Dbo/Dbo.h>
#include <Wt/Dbo/SqlConnectionPool.h>
#include <Wt/Auth/Dbo/UserDatabase>
#include <Wt/Auth/Login>
#include <Wt/Auth/PasswordService>
#include <Wt/Auth/Dbo/UserDatabase.h>
#include <Wt/Auth/Login.h>
#include <Wt/Auth/PasswordService.h>
#include "Types.hpp"
#include "User.hpp"
namespace Database {
typedef Wt::Auth::Dbo::UserDatabase<AuthInfo> UserDatabase;
using UserDatabase = Wt::Auth::Dbo::UserDatabase<AuthInfo>;
// Session living class handling the database and the login
class Handler
@@ -46,7 +46,8 @@ class Handler
Wt::Dbo::Session& getSession() { return _session; }
Wt::Dbo::ptr<User> getCurrentUser(); // get the current user, may return empty
Wt::Dbo::ptr<User> getUser(const Wt::Auth::User& authUser); // Get or create the given user
Wt::Dbo::ptr<User> getUser(const Wt::Auth::User& authUser);
Wt::Dbo::ptr<User> createUser(const Wt::Auth::User& authUser);
Wt::Auth::AbstractUserDatabase& getUserDatabase();
Wt::Auth::Login& getLogin() { return _login; }
@@ -57,7 +58,7 @@ class Handler
static const Wt::Auth::AuthService& getAuthService();
static const Wt::Auth::PasswordService& getPasswordService();
static Wt::Dbo::SqlConnectionPool* createConnectionPool(boost::filesystem::path db);
static std::unique_ptr<Wt::Dbo::SqlConnectionPool> createConnectionPool(boost::filesystem::path db);
private:
@@ -69,5 +70,4 @@ class Handler
} // namespace Database
#endif
-142
View File
@@ -1,142 +0,0 @@
/*
* Copyright (C) 2013 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <boost/foreach.hpp>
#include "Types.hpp"
static std::string pathsToString(const std::vector<boost::filesystem::path>& paths)
{
std::ostringstream oss;
bool first = true;
for (auto& path : paths)
{
if (!first)
oss << " ";
oss << path.string();
first = false;
}
return oss.str();
}
static std::vector<boost::filesystem::path> stringToPaths(const std::string value)
{
std::vector<std::string> res;
std::istringstream iss(value);
std::copy(std::istream_iterator<std::string>(iss), std::istream_iterator<std::string>(), std::back_inserter(res));
return std::vector<boost::filesystem::path>(res.begin(), res.end());
}
namespace Database {
MediaDirectorySettings::MediaDirectorySettings()
: _manualScanRequested(false),
_updatePeriod(Never),
_audioFileExtensions(".mp3 .ogg .oga .aac .m4a .flac .wav .wma .aif .aiff .ape .mpc .shn"),
_videoFileExtensions(".flv .avi .mpg .mpeg .mp4 .m4v .mkv .mov .wmv .ogv .divx .m2ts")
{
}
MediaDirectory::MediaDirectory(boost::filesystem::path p, Type type)
: _type(type),
_path(p.string())
{
}
MediaDirectorySettings::pointer
MediaDirectorySettings::get(Wt::Dbo::Session& session)
{
MediaDirectorySettings::pointer res;
res = session.find<MediaDirectorySettings>().where("id = ?").bind(1);
// TODO bind necessary?
if (!res)
res = session.add( new MediaDirectorySettings());
return res;
}
std::vector<boost::filesystem::path>
MediaDirectorySettings::getAudioFileExtensions(void) const
{
return stringToPaths(_audioFileExtensions);
}
std::vector<boost::filesystem::path>
MediaDirectorySettings::getVideoFileExtensions(void) const
{
return stringToPaths(_videoFileExtensions);
}
void
MediaDirectorySettings::setAudioFileExtensions(std::vector<boost::filesystem::path> extensions)
{
_audioFileExtensions = pathsToString(extensions);
}
void
MediaDirectorySettings::setVideoFileExtensions(std::vector<boost::filesystem::path> extensions)
{
_videoFileExtensions = pathsToString(extensions);
}
MediaDirectory::pointer
MediaDirectory::create(Wt::Dbo::Session& session, boost::filesystem::path p, Type type)
{
return session.add( new MediaDirectory( p, type ) );
}
void
MediaDirectory::eraseAll(Wt::Dbo::Session& session)
{
std::vector<MediaDirectory::pointer> dirs = getAll(session);
BOOST_FOREACH(MediaDirectory::pointer dir, dirs)
dir.remove();
}
std::vector<MediaDirectory::pointer>
MediaDirectory::getAll(Wt::Dbo::Session& session)
{
Wt::Dbo::collection< MediaDirectory::pointer > res = session.find<MediaDirectory>();
return std::vector<MediaDirectory::pointer>(res.begin(), res.end());
}
std::vector<MediaDirectory::pointer>
MediaDirectory::getByType(Wt::Dbo::Session& session, Type type)
{
Wt::Dbo::collection< MediaDirectory::pointer > res = session.find<MediaDirectory>().where("type = ?").bind (type);
return std::vector<MediaDirectory::pointer>(res.begin(), res.end());
}
MediaDirectory::pointer
MediaDirectory::get(Wt::Dbo::Session& session, boost::filesystem::path p, Type type)
{
return session.find<MediaDirectory>().where("path = ?").where("type = ?").bind( p.string()).bind(type);
}
} // namespace Database
-141
View File
@@ -1,141 +0,0 @@
/*
* Copyright (C) 2013 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef DATABASE_MEDIA_DIRECTORY_HPP
#define DATABASE_MEDIA_DIRECTORY_HPP
#include <vector>
#include <boost/filesystem/path.hpp>
#include <Wt/Dbo/Dbo>
#include <Wt/Dbo/WtSqlTraits>
namespace Database {
class MediaDirectory;
class MediaDirectorySettings
{
public:
enum UpdatePeriod {
Never,
Daily,
Weekly,
Monthly
};
typedef Wt::Dbo::ptr<MediaDirectorySettings> pointer;
MediaDirectorySettings();
// accessors
static pointer get(Wt::Dbo::Session& session);
// write accessors
void setManualScanRequested(bool value) { _manualScanRequested = value;}
void setUpdatePeriod(UpdatePeriod period) { _updatePeriod = period;}
void setUpdateStartTime(boost::posix_time::time_duration dur) { _updateStartTime = dur;}
void setLastUpdate(boost::posix_time::ptime time) { _lastUpdate = time; }
void setLastScan(boost::posix_time::ptime time) { _lastScan = time; }
void setAudioFileExtensions(std::vector<boost::filesystem::path> extensions);
void setVideoFileExtensions(std::vector<boost::filesystem::path> extensions);
// Read accessors
bool getManualScanRequested(void) const { return _manualScanRequested; }
UpdatePeriod getUpdatePeriod(void) const { return _updatePeriod; }
boost::posix_time::time_duration getUpdateStartTime(void) const { return _updateStartTime; }
boost::posix_time::ptime getLastUpdated(void) const { return _lastUpdate; }
boost::posix_time::ptime getLastScan(void) const { return _lastScan; }
std::vector<boost::filesystem::path> getAudioFileExtensions(void) const;
std::vector<boost::filesystem::path> getVideoFileExtensions(void) const;
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _manualScanRequested, "manual_scan_requested");
Wt::Dbo::field(a, _updatePeriod, "update_period");
Wt::Dbo::field(a, _updateStartTime, "update_start_time");
Wt::Dbo::field(a, _audioFileExtensions, "audio_file_extensions");
Wt::Dbo::field(a, _videoFileExtensions, "video_file_extensions");
Wt::Dbo::field(a, _lastUpdate, "last_update");
Wt::Dbo::field(a, _lastScan, "last_scan");
Wt::Dbo::hasMany(a, _mediaDirectories, Wt::Dbo::ManyToOne, "media_directory_settings");
}
private:
bool _manualScanRequested; // Immadiate scan has been requested by user
UpdatePeriod _updatePeriod; // How long between updates
boost::posix_time::time_duration _updateStartTime; // Time of day to begin the update
std::string _audioFileExtensions; // Extension of the audio files to be scanned
std::string _videoFileExtensions; // Extension of the video files to be scanned
boost::posix_time::ptime _lastUpdate; // last time the database has changed
boost::posix_time::ptime _lastScan; // last time the database has been scanned
Wt::Dbo::collection< Wt::Dbo::ptr<MediaDirectory> > _mediaDirectories; // list of media directories
};
class MediaDirectory
{
public:
typedef Wt::Dbo::ptr<MediaDirectory> pointer;
enum Type {
Audio = 1,
Video = 2,
};
MediaDirectory() {}
MediaDirectory(boost::filesystem::path p, Type type);
// Accessors
static pointer create(Wt::Dbo::Session& session, boost::filesystem::path p, Type type);
static std::vector<MediaDirectory::pointer> getAll(Wt::Dbo::Session& session);
static std::vector<MediaDirectory::pointer> getByType(Wt::Dbo::Session& session, Type type);
static pointer get(Wt::Dbo::Session& session, boost::filesystem::path p, Type type);
static void eraseAll(Wt::Dbo::Session& session);
Type getType(void) const { return _type; }
boost::filesystem::path getPath(void) const { return boost::filesystem::path(_path); }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _type, "type");
Wt::Dbo::field(a, _path, "path");
Wt::Dbo::belongsTo(a, _settings, "media_directory_settings", Wt::Dbo::OnDeleteCascade);
}
private:
Type _type;
std::string _path;
MediaDirectorySettings::pointer _settings; // back pointer
};
} // namespace Database
#endif
-92
View File
@@ -1,92 +0,0 @@
/*
* Copyright (C) 2014 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Types.hpp"
namespace Database {
Playlist::Playlist()
: _isPublic(false)
{
}
Playlist::Playlist(std::string name, bool isPublic, Wt::Dbo::ptr<User> user)
: _name(name),
_isPublic(isPublic),
_user(user)
{
}
Playlist::pointer
Playlist::create(Wt::Dbo::Session& session, std::string name, bool isPublic, Wt::Dbo::ptr<User> user)
{
return session.add( new Playlist(name, isPublic, user) );
}
PlaylistEntry::PlaylistEntry()
: _pos(0)
{
}
Playlist::pointer
Playlist::get(Wt::Dbo::Session& session, std::string name, Wt::Dbo::ptr<User> user)
{
return session.find<Playlist>().where("name = ? AND user_id = ?").bind(name).bind(user.id());
}
std::vector<Playlist::pointer>
Playlist::get(Wt::Dbo::Session& session, Wt::Dbo::ptr<User> user)
{
Wt::Dbo::collection<Playlist::pointer> res = session.find<Playlist>().where("user_id = ?").bind(user.id()).orderBy("name");
return std::vector<Playlist::pointer>(res.begin(), res.end());
}
PlaylistEntry::PlaylistEntry(Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<Playlist> playlist, int pos)
: _pos(pos),
_track(track),
_playlist(playlist)
{
}
PlaylistEntry::pointer
PlaylistEntry::create(Wt::Dbo::Session& session, Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<Playlist> playlist, int pos)
{
return session.add( new PlaylistEntry( track, playlist, pos) );
}
std::vector<Track::id_type>
PlaylistEntry::getEntries(Wt::Dbo::Session& session, Playlist::pointer playlist)
{
typedef Wt::Dbo::collection<pointer> Entries;
Entries entries = session.find<PlaylistEntry>().where("playlist_id = ?").bind(playlist.id()).orderBy("pos");
std::vector<Track::id_type> res;
for (Entries::iterator it = entries.begin(); it != entries.end(); ++it)
res.push_back((*it)->getTrack().id());
return res;
}
} // namespace Database
-108
View File
@@ -1,108 +0,0 @@
/*
* Copyright (C) 2014 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef DATABASE_PLAYLIST_HPP
#define DATABASE_PLAYLIST_HPP
#include <Wt/Dbo/Dbo>
#include <string>
namespace Database {
class PlaylistEntry;
class User;
class Track;
class Playlist
{
public:
typedef Wt::Dbo::ptr<Playlist> pointer;
Playlist();
Playlist(std::string name, bool isPublic, Wt::Dbo::ptr<User> user);
// Search utility
static pointer get(Wt::Dbo::Session& session, std::string name, Wt::Dbo::ptr<User> user);
// ordered by name
static std::vector<pointer> get(Wt::Dbo::Session& session, Wt::Dbo::ptr<User> user);
// Create utility
static pointer create(Wt::Dbo::Session& session, std::string name, bool isPublic, Wt::Dbo::ptr<User> user);
// Accessors
std::string getName() const { return _name; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::field(a, _isPublic, "public");
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _entries, Wt::Dbo::ManyToOne, "playlist");
}
private:
std::string _name;
bool _isPublic;
Wt::Dbo::ptr<User> _user;
Wt::Dbo::collection< Wt::Dbo::ptr<PlaylistEntry> > _entries;
};
class PlaylistEntry
{
public:
typedef Wt::Dbo::ptr<PlaylistEntry> pointer;
PlaylistEntry();
PlaylistEntry(Wt::Dbo::ptr<Track> rack, Wt::Dbo::ptr<Playlist> playlist, int position);
// Search utility
// Get the position ordered track id list
static std::vector<Track::id_type> getEntries(Wt::Dbo::Session& session,Wt::Dbo::ptr<Playlist> playlist);
// Create utility
static pointer create(Wt::Dbo::Session& session, Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<Playlist> playlist, int position);
// Accesors
Wt::Dbo::ptr<Track> getTrack() const { return _track; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _pos, "pos");
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _playlist, "playlist", Wt::Dbo::OnDeleteCascade);
}
private:
int _pos;
Wt::Dbo::ptr<Track> _track;
Wt::Dbo::ptr<Playlist> _playlist;
};
} // namespace Database
#endif
+184 -78
View File
@@ -17,9 +17,12 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Types.hpp"
#include "SearchFilter.hpp"
#include "Release.hpp"
#include "Artist.hpp"
#include "Cluster.hpp"
#include "SqlQuery.hpp"
#include "Track.hpp"
namespace Database
{
@@ -45,7 +48,7 @@ Release::getByMBID(Wt::Dbo::Session& session, const std::string& mbid)
}
Release::pointer
Release::getById(Wt::Dbo::Session& session, Release::id_type id)
Release::getById(Wt::Dbo::Session& session, IdType id)
{
return session.find<Release>().where("id = ?").bind(id);
}
@@ -53,23 +56,7 @@ Release::getById(Wt::Dbo::Session& session, Release::id_type id)
Release::pointer
Release::create(Wt::Dbo::Session& session, const std::string& name, const std::string& MBID)
{
return session.add(new Release(name, MBID));
}
bool
Release::isNone() const
{
return _name == "<None>";
}
Release::pointer
Release::getNone(Wt::Dbo::Session& session)
{
std::vector<pointer> res = getByName(session, "<None>");
if (res.empty())
return create(session, "<None>");
return res.front();
return session.add(std::make_unique<Release>(name, MBID));
}
std::vector<Release::pointer>
@@ -82,68 +69,75 @@ Release::getAll(Wt::Dbo::Session& session, int offset, int size)
std::vector<Release::pointer>
Release::getAllOrphans(Wt::Dbo::Session& session)
{
Wt::Dbo::collection<Release::pointer> res = session.query< Wt::Dbo::ptr<Release> >("select r from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL");
Wt::Dbo::collection<Release::pointer> res = session.query<Wt::Dbo::ptr<Release>>("select r from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL");
return std::vector<pointer>(res.begin(), res.end());
}
std::vector<Release::pointer>
Release::getLastAdded(Wt::Dbo::Session& session, Wt::WDateTime after, int limit)
{
Wt::Dbo::collection<Release::pointer> res = session.query<Release::pointer>("SELECT r from release r INNER JOIN track t ON r.id = t.release_id")
.where("t.file_added > ?").bind(after)
.groupBy("r.id")
.orderBy("t.file_added DESC")
.limit(limit);
return std::vector<pointer>(res.begin(), res.end());
}
static
Wt::Dbo::Query<Release::pointer>
Release::getQuery(Wt::Dbo::Session& session, SearchFilter filter)
getQuery(Wt::Dbo::Session& session,
const std::set<IdType>& clusterIds,
const std::vector<std::string> keywords)
{
SqlQuery sqlQuery = generatePartialQuery(filter);
WhereClause where;
Wt::Dbo::Query<pointer> query
= session.query<pointer>("SELECT r FROM release r INNER JOIN artist a ON a.id = t.artist_id INNER JOIN track t ON t.release_id = r.id INNER JOIN genre g ON g.id = t_g.genre_id INNER JOIN track_genre t_g ON t_g.track_id = t.id " + sqlQuery.where().get()).groupBy("r.id").orderBy("r.name");
std::ostringstream oss;
oss << "SELECT DISTINCT r FROM release r";
for (const std::string& bindArg : sqlQuery.where().getBindArgs())
query.bind(bindArg);
for (auto keyword : keywords)
where.And(WhereClause("r.name LIKE ?")).bind("%%" + keyword + "%%");
return query;
}
Wt::Dbo::Query<Release::UIQueryResult>
Release::getUIQuery(Wt::Dbo::Session& session, SearchFilter filter)
{
SqlQuery sqlQuery = generatePartialQuery(filter);
// TODO DATE of RELEASE
Wt::Dbo::Query<UIQueryResult> query
= session.query<UIQueryResult>("SELECT r.id, r.name, t.date, COUNT(DISTINCT t.id) FROM release r INNER JOIN track t ON t.release_id = r.id INNER JOIN artist a ON a.id = t.artist_id INNER JOIN genre g ON g.id = t_g.genre_id INNER JOIN track_genre t_g ON t_g.track_id = t.id " + sqlQuery.where().get()).groupBy("r.id").orderBy("r.name");
for (const std::string& bindArg : sqlQuery.where().getBindArgs())
query.bind(bindArg);
return query;
}
void
Release::updateUIQueryModel(Wt::Dbo::Session& session, Wt::Dbo::QueryModel<UIQueryResult>& model, SearchFilter filter, const std::vector<Wt::WString>& columnNames)
{
Wt::Dbo::Query<UIQueryResult> query = getUIQuery(session, filter);
model.setQuery(query, columnNames.empty() ? true : false);
// TODO do something better
if (columnNames.size() == 3)
if (!clusterIds.empty())
{
model.addColumn( "r.name", columnNames[0]);
model.addColumn( "t.date", columnNames[1]);
model.addColumn( "COUNT(DISTINCT t.id)", columnNames[2]);
oss << " INNER JOIN track t ON t.release_id = r.id INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
WhereClause clusterClause;
for (auto id : clusterIds)
clusterClause.Or(WhereClause("c.id = ?")).bind(std::to_string(id));
where.And(clusterClause);
}
oss << " " << where.get();
if (!clusterIds.empty())
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size();
oss << " ORDER BY r.name";
Wt::Dbo::Query<Release::pointer> query = session.query<Release::pointer>( oss.str() );
for (const std::string& bindArg : where.getBindArgs())
query.bind(bindArg);
return query;
}
std::vector<Release::pointer>
Release::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, int size)
Release::getByFilter(Wt::Dbo::Session& session,
const std::set<IdType>& clusterIds,
const std::vector<std::string> keywords,
int offset, int size, bool& moreResults)
{
Wt::Dbo::collection<pointer> res = getQuery(session, filter).limit(size).offset(offset);
Wt::Dbo::collection<pointer> collection = getQuery(session, clusterIds, keywords)
.limit(size != -1 ? size + 1 : -1)
.offset(offset);
return std::vector<pointer>(res.begin(), res.end());
}
std::vector<Release::pointer>
Release::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, int size, bool& moreResults)
{
auto res = getByFilter(session, filter, offset, size + 1);
auto res = std::vector<pointer>(collection.begin(), collection.end());
if (size != -1 && res.size() == static_cast<std::size_t>(size) + 1)
{
@@ -156,28 +150,140 @@ Release::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset,
return res;
}
int
boost::optional<int>
Release::getReleaseYear(bool original) const
{
assert(session());
// TODO something better
auto tracks = Track::getByFilter(*session(), SearchFilter::ById(SearchFilter::Field::Release, this->id()), -1, 1);
std::string field = original ? "t.original_year" : "t.year";
if (tracks.empty())
return 0;
Wt::Dbo::collection<int> dates = session()->query<int>(
std::string("SELECT ") + field + " FROM track t INNER JOIN release r ON r.id = t.release_id")
.where("r.id = ?")
.groupBy(field)
.bind(this->id());
boost::gregorian::date date;
/* various dates, no date */
if (dates.empty() || dates.size() > 1)
return boost::none;
if (original)
date = tracks.front()->getOriginalDate().date();
auto date = dates.front();
if (date > 0)
return date;
else
date = tracks.front()->getDate().date();
return boost::none;
}
if (date.is_special())
return 0;
std::vector<Wt::Dbo::ptr<Artist>>
Release::getArtists() const
{
assert(self());
assert(IdIsValid(self()->id()));
assert(session());
return date.year();
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> res = session()->query<Wt::Dbo::ptr<Artist>>(
"SELECT DISTINCT a FROM artist a INNER JOIN release r ON t.artist_id = a.id INNER JOIN track t ON t.release_id = r.id")
.where("r.id = ?")
.bind(id());
return std::vector<Wt::Dbo::ptr<Artist>>(res.begin(), res.end());
}
bool
Release::hasVariousArtists() const
{
// TODO optimize
return getArtists().size() > 1;
}
std::vector<Wt::Dbo::ptr<Track>>
Release::getTracks(const std::set<IdType>& clusterIds) const
{
assert(self());
assert(self()->id() != Wt::Dbo::dbo_traits<Release>::invalidId() );
assert(session());
WhereClause where;
std::ostringstream oss;
oss << "SELECT t FROM track t INNER JOIN release r ON t.release_id = r.id";
if (!clusterIds.empty())
{
oss << " INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
WhereClause clusterClause;
for (auto id : clusterIds)
clusterClause.Or(WhereClause("c.id = ?")).bind(std::to_string(id));
where.And(clusterClause);
}
where.And(WhereClause("r.id = ?")).bind(std::to_string(id()));
oss << " " << where.get();
if (!clusterIds.empty())
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size();
oss << " ORDER BY t.disc_number,t.track_number";
Wt::Dbo::Query<Track::pointer> query = session()->query<Track::pointer>( oss.str() );
for (const std::string& bindArg : where.getBindArgs())
{
query.bind(bindArg);
}
Wt::Dbo::collection< Wt::Dbo::ptr<Track> > res = query;
return std::vector< Wt::Dbo::ptr<Track> > (res.begin(), res.end());
}
std::vector<std::vector<Wt::Dbo::ptr<Cluster>>>
Release::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, std::size_t size) const
{
assert(self());
assert(self()->id() != Wt::Dbo::dbo_traits<Artist>::invalidId() );
assert(session());
WhereClause where;
std::ostringstream oss;
oss << "SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN cluster_type c_type ON c.cluster_type_id = c_type.id INNER JOIN release r ON t.release_id = r.id ";
where.And(WhereClause("r.id = ?")).bind(std::to_string(self()->id()));
{
WhereClause clusterClause;
for (auto clusterType : clusterTypes)
clusterClause.Or(WhereClause("c_type.id = ?")).bind(std::to_string(clusterType.id()));
where.And(clusterClause);
}
oss << " " << where.get();
oss << " GROUP BY c.id ORDER BY COUNT(c.id) DESC";
Wt::Dbo::Query<Cluster::pointer> query = session()->query<Cluster::pointer>( oss.str() );
for (const std::string& bindArg : where.getBindArgs())
query.bind(bindArg);
Wt::Dbo::collection<Cluster::pointer> queryRes = query;
std::map<IdType, std::vector<Cluster::pointer>> clusters;
for (auto cluster : queryRes)
{
if (clusters[cluster->getType().id()].size() < size)
clusters[cluster->getType().id()].push_back(cluster);
}
std::vector<std::vector<Cluster::pointer>> res;
for (auto cluster_list : clusters)
res.push_back(cluster_list.second);
return res;
}
} // namespace Database
+31 -25
View File
@@ -17,26 +17,28 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _DB_RELEASE_HPP_
#define _DB_RELEASE_HPP_
#pragma once
#include <Wt/Dbo/Dbo>
#include <Wt/Dbo/QueryModel>
#include <boost/optional.hpp>
#include "SearchFilter.hpp"
#include <Wt/Dbo/WtSqlTraits.h>
#include "Types.hpp"
namespace Database
{
class Track;
class Release;
class Artist;
class Cluster;
class ClusterType;
class Release : public Wt::Dbo::Dbo<Release>
{
public:
typedef Wt::Dbo::ptr<Release> pointer;
typedef Wt::Dbo::dbo_traits<Release>::IdType id_type;
Release() {}
Release(const std::string& name, const std::string& MBID = "");
@@ -44,31 +46,38 @@ class Release : public Wt::Dbo::Dbo<Release>
// Accessors
static pointer getByMBID(Wt::Dbo::Session& session, const std::string& MBID);
static std::vector<pointer> getByName(Wt::Dbo::Session& session, const std::string& name);
static pointer getById(Wt::Dbo::Session& session, id_type id);
static pointer getNone(Wt::Dbo::Session& session); // Special entry
static std::vector<pointer> getAllOrphans(Wt::Dbo::Session& session);
static pointer getById(Wt::Dbo::Session& session, IdType id);
static std::vector<pointer> getAllOrphans(Wt::Dbo::Session& session); // no track related
static std::vector<pointer> getAll(Wt::Dbo::Session& session, int offset, int size);
static std::vector<pointer> getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset = -1, int size = -1);
static std::vector<pointer> getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, int size, bool& moreExpected);
static std::vector<pointer> getLastAdded(Wt::Dbo::Session& session, Wt::WDateTime after, int size = 1);
static std::vector<pointer> getByFilter(Wt::Dbo::Session& session,
const std::set<IdType>& clusters, // at least one track that belongs to these clusters
const std::vector<std::string> keywords, // name must match all of these keywords
int offset,
int size,
bool& moreExpected);
std::vector<Wt::Dbo::ptr<Track>> getTracks(const std::set<IdType>& clusters = std::set<IdType>()) const;
// Get the cluster of the tracks that belong to this release
// Each clusters are grouped by cluster type, sorted by the number of occurence
// size is the max number of cluster per cluster type
std::vector<std::vector<Wt::Dbo::ptr<Cluster>>> getClusterGroups(std::vector<Wt::Dbo::ptr<ClusterType>> clusterTypes, std::size_t size) const;
// Create
static pointer create(Wt::Dbo::Session& session, const std::string& name, const std::string& MBID = "");
// Utility functions
int getReleaseYear(bool originalDate = false) const; // 0 if unknown or various
boost::optional<int> getReleaseYear(bool originalDate = false) const; // 0 if unknown or various
// MVC models for the user interface
// ID, Release name, year, track counts
typedef boost::tuple<id_type, std::string, boost::posix_time::ptime, int> UIQueryResult;
static Wt::Dbo::Query<UIQueryResult> getUIQuery(Wt::Dbo::Session& session, SearchFilter filter);
static void updateUIQueryModel(Wt::Dbo::Session& session, Wt::Dbo::QueryModel< UIQueryResult >& model, SearchFilter filter, const std::vector<Wt::WString>& columnNames = std::vector<Wt::WString>());
// Accessosrs
// Accessors
std::string getName() const { return _name; }
std::string getMBID() const { return _MBID; }
bool isNone(void) const;
boost::posix_time::time_duration getDuration(void) const;
std::chrono::seconds getDuration(void) const;
// Get the artists of this release
std::vector<Wt::Dbo::ptr<Artist> > getArtists() const;
bool hasVariousArtists() const;
void setMBID(std::string mbid) { _MBID = mbid; }
@@ -82,8 +91,6 @@ class Release : public Wt::Dbo::Dbo<Release>
}
private:
static Wt::Dbo::Query<pointer> getQuery(Wt::Dbo::Session& session, SearchFilter filter);
static const std::size_t _maxNameLength = 128;
std::string _name;
@@ -94,5 +101,4 @@ class Release : public Wt::Dbo::Dbo<Release>
} // namespace Database
#endif
+113
View File
@@ -0,0 +1,113 @@
/*
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ScanSettings.hpp"
#include <Wt/Dbo/WtSqlTraits.h>
#include "utils/Logger.hpp"
#include "utils/Utils.hpp"
#include "Cluster.hpp"
namespace {
std::set<std::string> defaultClusterTypeNames =
{
"GENRE",
"ALBUMGROUPING",
"MOOD",
"ALBUMMOOD",
"COMMENT:SONGS-DB_OCCASION",
};
}
namespace Database {
ScanSettings::pointer
ScanSettings::get(Wt::Dbo::Session& session)
{
ScanSettings::pointer settings = session.find<ScanSettings>();
if (!settings)
{
settings = session.add(std::make_unique<ScanSettings>());
settings.modify()->setClusterTypes(defaultClusterTypeNames);
}
return settings;
}
std::set<boost::filesystem::path>
ScanSettings::getAudioFileExtensions() const
{
auto extensions = splitString(_audioFileExtensions, " ");
return std::set<boost::filesystem::path>(extensions.begin(), extensions.end());
}
std::vector<ClusterType::pointer>
ScanSettings::getClusterTypes() const
{
return std::vector<ClusterType::pointer>(_clusterTypes.begin(), _clusterTypes.end());
}
void
ScanSettings::setMediaDirectory(boost::filesystem::path p)
{
_mediaDirectory = stringTrimEnd(p.string(), "/\\");
}
void
ScanSettings::setClusterTypes(const std::set<std::string>& clusterTypeNames)
{
bool needRescan = false;
assert(session());
// Create any missing cluster type
for (auto clusterTypeName : clusterTypeNames)
{
auto clusterType = ClusterType::getByName(*session(), clusterTypeName);
if (!clusterType)
{
LMS_LOG(DB, INFO) << "Creating cluster type " << clusterTypeName;
clusterType = ClusterType::create(*session(), clusterTypeName);
needRescan = true;
}
_clusterTypes.insert(clusterType);
}
// Delete no longer existing cluster types
for (auto clusterType : _clusterTypes)
{
if (std::none_of(clusterTypeNames.begin(), clusterTypeNames.end(),
[clusterType](const std::string& name) { return name == clusterType->getName(); }))
{
LMS_LOG(DB, INFO) << "Deleting cluster type " << clusterType->getName();
clusterType.remove();
needRescan = true;
}
}
if (needRescan)
_scanVersion += 1;
}
} // namespace Database
+84
View File
@@ -0,0 +1,84 @@
/*
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <boost/filesystem.hpp>
#include <Wt/Dbo/Dbo.h>
#include <Wt/WTime.h>
namespace Database {
class ClusterType;
class ScanSettings : public Wt::Dbo::Dbo<ScanSettings>
{
public:
using pointer = Wt::Dbo::ptr<ScanSettings>;
enum class UpdatePeriod {
Never = 0,
Daily,
Weekly,
Monthly
};
ScanSettings() {}
static pointer get(Wt::Dbo::Session& session);
// Getters
std::size_t getScanVersion() const { return _scanVersion; }
boost::filesystem::path getMediaDirectory() const { return _mediaDirectory; }
Wt::WTime getUpdateStartTime() const { return _startTime; }
UpdatePeriod getUpdatePeriod() const { return _updatePeriod; }
std::vector<Wt::Dbo::ptr<ClusterType>> getClusterTypes() const;
std::set<boost::filesystem::path> getAudioFileExtensions() const;
// Setters
void setMediaDirectory(boost::filesystem::path p);
void setUpdateStartTime(Wt::WTime t) { _startTime = t; }
void setUpdatePeriod(UpdatePeriod p) { _updatePeriod = p; }
void setClusterTypes(const std::set<std::string>& clusterTypeNames);
void setAudioFileExtensions(std::set<boost::filesystem::path> fileExtensions);
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _scanVersion, "scan_version");
Wt::Dbo::field(a, _mediaDirectory, "media_directory");
Wt::Dbo::field(a, _startTime, "start_time");
Wt::Dbo::field(a, _updatePeriod, "update_period");
Wt::Dbo::field(a, _audioFileExtensions, "audio_file_extensions");
Wt::Dbo::hasMany(a, _clusterTypes, Wt::Dbo::ManyToOne, "scan_settings");
}
private:
int _scanVersion = 0;
std::string _mediaDirectory = "";
Wt::WTime _startTime = Wt::WTime(0,0,0);
UpdatePeriod _updatePeriod = UpdatePeriod::Never;
std::string _audioFileExtensions = ".mp3 .ogg .oga .aac .m4a .flac .wav .wma .aif .aiff .ape .mpc .shn";
Wt::Dbo::collection<Wt::Dbo::ptr<ClusterType>> _clusterTypes;
};
} // namespace Database
-123
View File
@@ -1,123 +0,0 @@
/*
* Copyright (C) 2015 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "logger/Logger.hpp"
#include "SearchFilter.hpp"
namespace Database
{
static std::ostream& operator<<(std::ostream& ost, const std::vector< Wt::Dbo::dbo_default_traits::IdType>& ids)
{
const char *sep = "";
for (auto id : ids)
{
ost << sep << std::to_string(id);
sep = ",";
}
return ost;
}
SqlQuery generatePartialQuery(SearchFilter& filter)
{
SqlQuery sqlQuery;
// Process name like parameters
for (auto nameLikeMatches : filter.nameLikeMatch)
{
WhereClause likeWhereClause;
for (auto nameLikeMatch : nameLikeMatches)
{
switch (nameLikeMatch.first)
{
case SearchFilter::Field::Artist:
for (const std::string& name : nameLikeMatch.second)
likeWhereClause.Or( WhereClause("a.name LIKE ?") ).bind("%%" + name + "%%");
break;
case SearchFilter::Field::Release:
for (const std::string& name : nameLikeMatch.second)
likeWhereClause.Or( WhereClause("r.name LIKE ?") ).bind("%%" + name + "%%");
break;
case SearchFilter::Field::Genre:
for (const std::string& name : nameLikeMatch.second)
likeWhereClause.Or( WhereClause("g.name LIKE ?") ).bind("%%" + name + "%%");
break;
case SearchFilter::Field::Track:
for (const std::string& name : nameLikeMatch.second)
likeWhereClause.Or( WhereClause("t.name LIKE ?") ).bind("%%" + name + "%%");
break;
}
}
sqlQuery.where().And( likeWhereClause );
}
// Process id exact match parameters
// Id list may be long, we do not bind the parameters
// Safe since we already known these are just ints
for (auto idMatch : filter.idMatch)
{
WhereClause idWhereClause;
switch (idMatch.first)
{
case SearchFilter::Field::Artist:
{
std::ostringstream oss;
oss << "a.id IN (" << idMatch.second << ")";
idWhereClause.Or(oss.str());
}
break;
case SearchFilter::Field::Release:
{
std::ostringstream oss;
oss << "r.id IN (" << idMatch.second << ")";
idWhereClause.Or(oss.str());
}
break;
case SearchFilter::Field::Genre:
{
std::ostringstream oss;
oss << "g.id IN (" << idMatch.second << ")";
idWhereClause.Or(oss.str());
}
break;
case SearchFilter::Field::Track:
{
std::ostringstream oss;
oss << "t.id IN (" << idMatch.second << ")";
idWhereClause.Or(oss.str());
}
break;
}
sqlQuery.where().And( idWhereClause );
}
return sqlQuery;
}
} // namespace Database
-102
View File
@@ -1,102 +0,0 @@
/*
* Copyright (C) 2015 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _DB_SEARCH_FILTER_HPP_
#define _DB_SEARCH_FILTER_HPP_
#include <string>
#include <vector>
#include <map>
#include <Wt/Dbo/Dbo>
#include "SqlQuery.hpp"
namespace Database
{
class SearchFilter
{
public:
enum class Field {
Artist, // artist
Release, // release
Genre, // genre
Track, // track
};
typedef std::vector<std::map<Field, std::vector<std::string> > > NameLikeMatchType;
typedef std::map<Field, std::vector< Wt::Dbo::dbo_default_traits::IdType> > IdMatchType;
SearchFilter() {}
// Helpers
static SearchFilter IdMatch( const IdMatchType& _idMatch )
{
return SearchFilter(_idMatch);
}
static SearchFilter NameLikeMatch( const NameLikeMatchType& _nameLikeMatch )
{
return SearchFilter(_nameLikeMatch);
}
// Single Field ID match
static SearchFilter ById(Field field, Wt::Dbo::dbo_default_traits::IdType id)
{
return SearchFilter({{field, {id} }});
}
// Single Field Name match OR
static SearchFilter ByNameOr(Field field, std::vector<std::string> keywords)
{
return NameLikeMatch({{{field, keywords}}});
}
// Single Field Name match AND
static SearchFilter ByNameAnd(Field field, std::vector<std::string> keywords)
{
NameLikeMatchType nameLikeMatch;
for (auto keyword : keywords)
nameLikeMatch.push_back({{{field, {keyword}}}});
return NameLikeMatch(nameLikeMatch);
}
// The filter is a AND of the following conditions:
// ((Field1.name LIKE STR1-1 OR Field1.name LIKE STR1-2 ...) OR (Field2.name LIKE STR2-1 OR Field2.name LIKE STR2-2 ...) ...
NameLikeMatchType nameLikeMatch;
// (Field1.id IN (ID1-1,ID1-2 ... ) AND (Field2.id IN (ID2-1,ID2-2 ... ) ...
IdMatchType idMatch;
private:
SearchFilter(const NameLikeMatchType& _nameLikeMatch) : nameLikeMatch(_nameLikeMatch) {}
SearchFilter(const IdMatchType& _idMatch) : idMatch(_idMatch) {}
};
SqlQuery generatePartialQuery(SearchFilter& filter);
} // namespace Database
#endif // _DB_SEARCH_FILTER_HPP_
+11 -12
View File
@@ -17,15 +17,12 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <algorithm>
#include <boost/foreach.hpp>
#include <sstream>
#include <stdexcept>
#include "SqlQuery.hpp"
#include <algorithm>
#include <cassert>
#include <sstream>
WhereClause&
WhereClause::And(const WhereClause& otherClause)
{
@@ -35,7 +32,8 @@ WhereClause::And(const WhereClause& otherClause)
_clause += "(" + otherClause._clause + ")";
// Add associated bind args
BOOST_FOREACH(const std::string& otherBindArg, otherClause._bindArgs) {
for (const std::string& otherBindArg : otherClause._bindArgs)
{
_bindArgs.push_back(otherBindArg);
}
}
@@ -51,7 +49,8 @@ WhereClause::Or(const WhereClause& otherClause)
_clause += "(" + otherClause._clause + ")";
// Add associated bind args
BOOST_FOREACH(const std::string& otherBindArg, otherClause._bindArgs) {
for (const std::string& otherBindArg : otherClause._bindArgs)
{
_bindArgs.push_back(otherBindArg);
}
}
@@ -70,8 +69,7 @@ WhereClause::get(void) const
WhereClause&
WhereClause::bind(const std::string& bindArg)
{
if (_bindArgs.size() >= static_cast<std::size_t>( std::count(_clause.begin(), _clause.end(), '?') ))
throw std::runtime_error("Too many bind args!");
assert(_bindArgs.size() < static_cast<std::size_t>(std::count(_clause.begin(), _clause.end(), '?')));
_bindArgs.push_back(bindArg);
@@ -147,7 +145,8 @@ FromClause::FromClause(const std::string& clause)
FromClause&
FromClause::And(const FromClause& clause)
{
BOOST_FOREACH(const std::string fromClause, clause._clause) {
for (const std::string fromClause : clause._clause)
{
_clause.push_back(fromClause);
}
+1 -4
View File
@@ -17,8 +17,7 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef SQL_QUERY_HPP___
#define SQL_QUERY_HPP___
#pragma once
#include <list>
#include <string>
@@ -134,5 +133,3 @@ class SqlQuery
GroupByStatement _groupByStatement; // GROUP BY statement
};
#endif
+160 -183
View File
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2013 Emeric Poupon
* Copyright (C) 2013-2016 Emeric Poupon
*
* This file is part of LMS.
*
@@ -17,24 +17,22 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <Wt/Dbo/QueryModel>
#include "Track.hpp"
#include "logger/Logger.hpp"
#include <Wt/Dbo/WtSqlTraits.h>
#include "utils/Logger.hpp"
#include "Artist.hpp"
#include "Cluster.hpp"
#include "Release.hpp"
#include "SqlQuery.hpp"
#include "Types.hpp"
namespace Database {
Track::Track(const boost::filesystem::path& p)
:
_trackNumber(0),
_totalTrackNumber(0),
_discNumber(0),
_totalDiscNumber(0),
_filePath( p.string() ),
_coverType(CoverType::None)
_filePath( p.string() )
{
}
@@ -44,15 +42,12 @@ Track::getAll(Wt::Dbo::Session& session)
return session.find<Track>();
}
void
Track::setGenres(std::vector<Genre::pointer> genres)
std::vector<IdType>
Track::getAllIds(Wt::Dbo::Session& session)
{
if (_genres.size())
_genres.clear();
for (Genre::pointer genre : genres) {
_genres.insert( genre );
}
Wt::Dbo::Transaction transaction(session);
Wt::Dbo::collection<IdType> res = session.query<IdType>("SELECT id from track");
return std::vector<IdType>(res.begin(), res.end());
}
Track::pointer
@@ -62,7 +57,7 @@ Track::getByPath(Wt::Dbo::Session& session, const boost::filesystem::path& p)
}
Track::pointer
Track::getById(Wt::Dbo::Session& session, id_type id)
Track::getById(Wt::Dbo::Session& session, IdType id)
{
return session.find<Track>().where("id = ?").bind(id);
}
@@ -76,7 +71,7 @@ Track::getByMBID(Wt::Dbo::Session& session, const std::string& mbid)
Track::pointer
Track::create(Wt::Dbo::Session& session, const boost::filesystem::path& p)
{
return session.add(new Track(p) );
return session.add(std::make_unique<Track>(p));
}
std::vector<boost::filesystem::path>
@@ -101,68 +96,77 @@ Track::getChecksumDuplicates(Wt::Dbo::Session& session)
return std::vector<pointer>(res.begin(), res.end());
}
std::vector< Genre::pointer >
Track::getGenres(void) const
{
std::vector< Genre::pointer > genres;
std::copy(_genres.begin(), _genres.end(), std::back_inserter(genres));
return genres;
}
Wt::Dbo::Query< Track::pointer >
Track::getQuery(Wt::Dbo::Session& session, SearchFilter filter)
{
SqlQuery sqlQuery = generatePartialQuery(filter);
Wt::Dbo::Query<pointer> query
= session.query<pointer>( "SELECT t FROM track t INNER JOIN artist a ON t.artist_id = a.id INNER JOIN genre g ON g.id = t_g.genre_id INNER JOIN track_genre t_g ON t_g.track_id = t.id INNER JOIN release r ON r.id = t.release_id " + sqlQuery.where().get()).groupBy("t.id").orderBy("a.name,t.date,r.name,t.disc_number,t.track_number");
for (const std::string& bindArg : sqlQuery.where().getBindArgs())
query.bind(bindArg);
return query;
}
Wt::Dbo::Query< Track::UIQueryResult >
Track::getUIQuery(Wt::Dbo::Session& session, SearchFilter filter)
{
SqlQuery sqlQuery = generatePartialQuery(filter);
Wt::Dbo::Query<UIQueryResult> query
= session.query<UIQueryResult>( "SELECT t.id, a.name, r.name, t.disc_number, t.track_number, t.name, t.duration, t.date, t.original_date, t.genre_list FROM track t INNER JOIN artist a ON t.artist_id = a.id INNER JOIN genre g ON g.id = t_g.genre_id INNER JOIN track_genre t_g ON t_g.track_id = t.id INNER JOIN release r ON r.id = t.release_id " + sqlQuery.where().get()).groupBy("t.id").orderBy("a.name,t.date,r.name,t.disc_number,t.track_number");
for (const std::string& bindArg : sqlQuery.where().getBindArgs())
query.bind(bindArg);
return query;
}
Track::StatsQueryResult
Track::getStats(Wt::Dbo::Session& session, SearchFilter filter)
{
SqlQuery sqlQuery = generatePartialQuery(filter);
Wt::Dbo::Query<StatsQueryResult> query = session.query<StatsQueryResult>( "SELECT COUNT(\"id\"), SUM(\"dur\") FROM (SELECT t.id as \"id\", t.duration as \"dur\" FROM track t INNER JOIN artist a ON t.artist_id = a.id INNER JOIN genre g ON g.id = t_g.genre_id INNER JOIN track_genre t_g ON t_g.track_id = t.id INNER JOIN release r ON r.id = t.release_id " + sqlQuery.where().get() + " GROUP BY t.id)");
for (const std::string& bindArg : sqlQuery.where().getBindArgs())
query.bind(bindArg);
return query;
}
std::vector<Track::pointer>
Track::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, int size)
Track::getLastAdded(Wt::Dbo::Session& session, Wt::WDateTime after, int limit)
{
Wt::Dbo::collection<pointer> res = getQuery(session, filter).limit(size).offset(offset);
Wt::Dbo::collection<Track::pointer> res = session.find<Track>()
.where("file_added > ?").bind(after)
.orderBy("file_added DESC")
.limit(limit);
return std::vector<pointer>(res.begin(), res.end());
}
std::vector<Track::pointer>
Track::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, int size, bool& moreResults)
std::vector<Cluster::pointer>
Track::getClusters(void) const
{
auto res = getByFilter(session, filter, offset, size + 1);
std::vector< Cluster::pointer > clusters;
std::copy(_clusters.begin(), _clusters.end(), std::back_inserter(clusters));
return clusters;
}
static
Wt::Dbo::Query< Track::pointer >
getQuery(Wt::Dbo::Session& session,
const std::set<IdType>& clusterIds,
const std::vector<std::string> keywords)
{
WhereClause where;
std::ostringstream oss;
oss << "SELECT t FROM track t";
for (auto keyword : keywords)
where.And(WhereClause("t.name LIKE ?")).bind("%%" + keyword + "%%");
if (!clusterIds.empty())
{
oss << " INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
WhereClause clusterClause;
for (auto id : clusterIds)
clusterClause.Or(WhereClause("c.id = ?")).bind(std::to_string(id));
where.And(clusterClause);
}
oss << " " << where.get();
if (!clusterIds.empty())
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size();
oss << " ORDER BY t.name";
Wt::Dbo::Query<Track::pointer> query = session.query<Track::pointer>( oss.str() );
for (const std::string& bindArg : where.getBindArgs())
query.bind(bindArg);
return query;
}
std::vector<Track::pointer>
Track::getByFilter(Wt::Dbo::Session& session,
const std::set<IdType>& clusterIds,
const std::vector<std::string> keywords,
int offset, int size, bool& moreResults)
{
Wt::Dbo::collection<pointer> collection = getQuery(session, clusterIds, keywords)
.limit(size != -1 ? size + 1 : -1)
.offset(offset);
auto res = std::vector<pointer>(collection.begin(), collection.end());
if (size != -1 && res.size() == static_cast<std::size_t>(size) + 1)
{
@@ -175,121 +179,94 @@ Track::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, i
return res;
}
void
Track::updateUIQueryModel(Wt::Dbo::Session& session, Wt::Dbo::QueryModel< UIQueryResult >& model, SearchFilter filter, const std::vector<Wt::WString>& columnNames)
std::vector<Track::pointer>
Track::getByFilter(Wt::Dbo::Session& session,
const std::set<IdType>& clusters)
{
Wt::Dbo::Query< UIQueryResult > query = getUIQuery(session, filter);
model.setQuery(query, columnNames.empty() ? true : false);
bool moreResults;
// TODO do something better
if (columnNames.size() == 9)
return getByFilter(session, clusters, std::vector<std::string>(), -1, -1, moreResults);
}
boost::optional<std::size_t>
Track::getTrackNumber(void) const
{
return (_trackNumber > 0) ? boost::make_optional<std::size_t>(_trackNumber) : boost::none;
}
boost::optional<std::size_t>
Track::getTotalTrackNumber(void) const
{
return (_totalTrackNumber > 0) ? boost::make_optional<std::size_t>(_totalTrackNumber) : boost::none;
}
boost::optional<std::size_t>
Track::getDiscNumber(void) const
{
return (_discNumber > 0) ? boost::make_optional<std::size_t>(_discNumber) : boost::none;
}
boost::optional<std::size_t>
Track::getTotalDiscNumber(void) const
{
return (_totalDiscNumber > 0) ? boost::make_optional<std::size_t>(_totalDiscNumber) : boost::none;
}
boost::optional<int>
Track::getYear() const
{
return (_year > 0) ? boost::make_optional<int>(_year) : boost::none;
}
boost::optional<int>
Track::getOriginalYear() const
{
return (_originalYear > 0) ? boost::make_optional<int>(_originalYear) : boost::none;
}
std::vector<std::vector<Cluster::pointer>>
Track::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, std::size_t size) const
{
assert(self());
assert(IdIsValid(self()->id()));
assert(session());
WhereClause where;
std::ostringstream oss;
oss << "SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN cluster_type c_type ON c.cluster_type_id = c_type.id";
where.And(WhereClause("t.id = ?")).bind(std::to_string(self()->id()));
{
model.addColumn( "a.name", columnNames[0] );
model.addColumn( "r.name", columnNames[1] );
model.addColumn( "t.disc_number", columnNames[2] );
model.addColumn( "t.track_number", columnNames[3] );
model.addColumn( "t.name", columnNames[4] );
model.addColumn( "t.duration", columnNames[5] );
model.addColumn( "t.date", columnNames[6] );
model.addColumn( "t.original_date", columnNames[7] );
model.addColumn( "t.genre_list", columnNames[8] );
WhereClause clusterClause;
for (auto clusterType : clusterTypes)
clusterClause.Or(WhereClause("c_type.id = ?")).bind(std::to_string(clusterType.id()));
where.And(clusterClause);
}
oss << " " << where.get();
oss << " GROUP BY c.id ORDER BY COUNT(c.id) DESC";
Wt::Dbo::Query<Cluster::pointer> query = session()->query<Cluster::pointer>( oss.str() );
for (const std::string& bindArg : where.getBindArgs())
query.bind(bindArg);
Wt::Dbo::collection<Cluster::pointer> queryRes = query;
std::map<IdType, std::vector<Cluster::pointer>> clusters;
for (auto cluster : queryRes)
{
if (clusters[cluster->getType().id()].size() < size)
clusters[cluster->getType().id()].push_back(cluster);
}
}
std::vector<std::vector<Cluster::pointer>> res;
for (auto cluster_list : clusters)
res.push_back(cluster_list.second);
Genre::Genre()
{
}
Genre::Genre(const std::string& name)
: _name( std::string(name, 0, _maxNameLength) )
{
}
Wt::Dbo::collection<Genre::pointer>
Genre::getAll(Wt::Dbo::Session& session)
{
return session.find<Genre>();
}
Genre::pointer
Genre::getByName(Wt::Dbo::Session& session, const std::string& name)
{
// TODO use like search
return session.find<Genre>().where("name = ?").bind( std::string(name, 0, _maxNameLength) );
}
Genre::pointer
Genre::getNone(Wt::Dbo::Session& session)
{
pointer res = getByName(session, "<None>");
if (!res)
res = create(session, "<None>");
return res;
}
bool
Genre::isNone(void) const
{
return (_name == "<None>");
}
Genre::pointer
Genre::create(Wt::Dbo::Session& session, const std::string& name)
{
return session.add(new Genre(name));
}
Wt::Dbo::Query<Genre::pointer>
Genre::getQuery(Wt::Dbo::Session& session, SearchFilter filter)
{
SqlQuery sqlQuery = generatePartialQuery(filter);
Wt::Dbo::Query<pointer> query
= session.query<pointer>( "SELECT g FROM genre g INNER JOIN track_genre t_g ON t_g.genre_id = g.id INNER JOIN artist a ON t.artist_id = a.id INNER JOIN release r ON r.id = t.release_id INNER JOIN track t ON t.id = t_g.track_id " + sqlQuery.where().get()).groupBy("g.name").orderBy("g.name");
for (const std::string& bindArg : sqlQuery.where().getBindArgs())
query.bind(bindArg);
return query;
}
Wt::Dbo::Query<Genre::UIQueryResult>
Genre::getUIQuery(Wt::Dbo::Session& session, SearchFilter filter)
{
SqlQuery sqlQuery = generatePartialQuery(filter);
Wt::Dbo::Query<UIQueryResult> query
= session.query<UIQueryResult>( "SELECT g.id, g.name, COUNT(DISTINCT t.id) FROM genre g INNER JOIN track_genre t_g ON t_g.genre_id = g.id INNER JOIN artist a ON t.artist_id = a.id INNER JOIN release r ON r.id = t.release_id INNER JOIN track t ON t.id = t_g.track_id " + sqlQuery.where().get()).groupBy("g.name").orderBy("g.name");
for (const std::string& bindArg : sqlQuery.where().getBindArgs())
query.bind(bindArg);
return query;
}
void
Genre::updateUIQueryModel(Wt::Dbo::Session& session, Wt::Dbo::QueryModel<UIQueryResult>& model, SearchFilter filter, const std::vector<Wt::WString>& columnNames)
{
Wt::Dbo::Query<UIQueryResult> query = getUIQuery(session, filter);
model.setQuery(query, columnNames.empty() ? true : false);
// TODO do something better
if (columnNames.size() == 2)
{
model.addColumn( "g.name", columnNames[0] );
model.addColumn( "COUNT(DISTINCT t.id)", columnNames[1] );
}
}
std::vector<Genre::pointer>
Genre::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, int size)
{
Wt::Dbo::collection<pointer> res = getQuery(session, filter).limit(size).offset(offset);
return std::vector<pointer>(res.begin(), res.end());
}
} // namespace Database
+72 -132
View File
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2013 Emeric Poupon
* Copyright (C) 2013-2016 Emeric Poupon
*
* This file is part of LMS.
*
@@ -17,79 +17,34 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _AUDIO_TYPES_HPP_
#define _AUDIO_TYPES_HPP_
#pragma once
#include <string>
#include <vector>
#include <chrono>
#include <boost/filesystem.hpp>
#include <boost/optional.hpp>
#include <Wt/Dbo/Dbo>
#include <Wt/Dbo/WtSqlTraits>
#include <Wt/Dbo/Dbo.h>
#include <Wt/WDateTime.h>
#include <Wt/WDateTime>
#include "Types.hpp"
namespace Database {
class Artist;
class Cluster;
class ClusterType;
class TrackListEntry;
class Release;
class Track;
class PlaylistEntry;
class TrackStats;
class Genre
{
public:
typedef Wt::Dbo::ptr<Genre> pointer;
typedef Wt::Dbo::dbo_traits<Genre>::IdType id_type;
Genre();
Genre(const std::string& name);
// Find utility
static pointer getByName(Wt::Dbo::Session& session, const std::string& name);
static pointer getNone(Wt::Dbo::Session& session);
static std::vector<pointer> getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset = -1, int size = -1);
static Wt::Dbo::collection<Genre::pointer> getAll(Wt::Dbo::Session& session);
// MVC models for the user interface
// Genre ID, name, track count
typedef boost::tuple<id_type, std::string, int> UIQueryResult;
static Wt::Dbo::Query<UIQueryResult> getUIQuery(Wt::Dbo::Session& session, SearchFilter filter);
static void updateUIQueryModel(Wt::Dbo::Session& session, Wt::Dbo::QueryModel<UIQueryResult>& model, SearchFilter filter, const std::vector<Wt::WString>& columnNames = std::vector<Wt::WString>());
// Create utility
static pointer create(Wt::Dbo::Session& session, const std::string& name);
// Accessors
const std::string& getName(void) const { return _name; }
bool isNone(void) const;
const Wt::Dbo::collection< Wt::Dbo::ptr<Track> >& getTracks() const { return _tracks;}
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_genre", "", Wt::Dbo::OnDeleteCascade);
}
private:
static Wt::Dbo::Query<pointer> getQuery(Wt::Dbo::Session& session, SearchFilter filter);
static const std::size_t _maxNameLength = 128;
std::string _name;
Wt::Dbo::collection< Wt::Dbo::ptr<Track> > _tracks;
};
class Track
class Track : public Wt::Dbo::Dbo<Track>
{
public:
typedef Wt::Dbo::ptr<Track> pointer;
typedef Wt::Dbo::dbo_traits<Track>::IdType id_type;
Track() {}
Track(const boost::filesystem::path& p);
@@ -102,91 +57,82 @@ class Track
// Find utility functions
static pointer getByPath(Wt::Dbo::Session& session, const boost::filesystem::path& p);
static pointer getById(Wt::Dbo::Session& session, id_type id);
static pointer getById(Wt::Dbo::Session& session, IdType id);
static pointer getByMBID(Wt::Dbo::Session& session, const std::string& MBID);
static std::vector<pointer> getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset = -1, int size = -1);
static std::vector<pointer> getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset, int size, bool &moreResults);
static std::vector<pointer> getByFilter(Wt::Dbo::Session& session,
const std::set<IdType>& clusters); // tracks that belong to these clusters
static std::vector<pointer> getByFilter(Wt::Dbo::Session& session,
const std::set<IdType>& clusters, // tracks that belong to these clusters
const std::vector<std::string> keywords, // name must match all of these keywords
int offset,
int size,
bool& moreExpected);
static Wt::Dbo::collection< pointer > getAll(Wt::Dbo::Session& session);
static std::vector<boost::filesystem::path> getAllPaths(Wt::Dbo::Session& session);
static std::vector<IdType> getAllIds(Wt::Dbo::Session& session); // nested transaction
static std::vector<boost::filesystem::path> getAllPaths(Wt::Dbo::Session& session); // nested transaction
static std::vector<pointer> getMBIDDuplicates(Wt::Dbo::Session& session);
static std::vector<pointer> getChecksumDuplicates(Wt::Dbo::Session& session);
// Utility fonctions
// MVC models for the user interface
// ID, Artist name, Release Name, DiscNumber, TrackNumber, Name, duration, date, original date, genre list
typedef boost::tuple<id_type, //ID
std::string, // Artist name
std::string, // Release Name
int, // Disc Number
int, // Track Number
std::string, // Name
boost::posix_time::time_duration, // Duration
boost::posix_time::ptime, // Date
boost::posix_time::ptime, // Original date
std::string> // genre list
UIQueryResult;
static Wt::Dbo::Query< UIQueryResult > getUIQuery(Wt::Dbo::Session& session, SearchFilter filter);
static void updateUIQueryModel(Wt::Dbo::Session& session, Wt::Dbo::QueryModel< UIQueryResult >& model, SearchFilter filter, const std::vector<Wt::WString>& columnNames = std::vector<Wt::WString>());
// Stats for a given search filter
typedef boost::tuple<
int, // Total tracks
boost::posix_time::time_duration // Total duration
> StatsQueryResult;
static StatsQueryResult getStats(Wt::Dbo::Session& session, SearchFilter filter);
static std::vector<pointer> getLastAdded(Wt::Dbo::Session& session, Wt::WDateTime after, int size = 1);
// Create utility
static pointer create(Wt::Dbo::Session& session, const boost::filesystem::path& p);
// Remove utility
static void removeClusters(std::string type);
// Accessors
void setScanVersion(std::size_t version) {_scanVersion = version; }
void setTrackNumber(int num) { _trackNumber = num; }
void setTotalTrackNumber(int num) { _totalTrackNumber = num; }
void setDiscNumber(int num) { _discNumber = num; }
void setTotalDiscNumber(int num) { _totalDiscNumber = num; }
void setName(const std::string& name) { _name = std::string(name, 0, _maxNameLength); }
void setDuration(boost::posix_time::time_duration duration) { _duration = duration; }
void setLastWriteTime(boost::posix_time::ptime time) { _fileLastWrite = time; }
void setAddedTime(boost::posix_time::ptime time) { _fileAdded = time; }
void setDuration(std::chrono::milliseconds duration) { _duration = duration; }
void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; }
void setAddedTime(Wt::WDateTime time) { _fileAdded = time; }
void setChecksum(const std::vector<unsigned char>& checksum) { _fileChecksum = checksum; }
void setDate(const boost::posix_time::ptime& date) { _date = date; }
void setOriginalDate(const boost::posix_time::ptime& date) { _originalDate = date; }
void setYear(int year) { _year = year; }
void setOriginalYear(int year) { _originalYear = year; }
void setGenres(const std::string& genreList) { _genreList = genreList; }
void setCoverType(CoverType coverType) { _coverType = coverType; }
void setMBID(const std::string& MBID) { _MBID = MBID; }
void setArtist(Wt::Dbo::ptr<Artist> artist) { _artist = artist; }
void setRelease(Wt::Dbo::ptr<Release> release) { _release = release; }
void setGenres(std::vector<Genre::pointer> genres);
int getTrackNumber(void) const { return _trackNumber; }
int getTotalTrackNumber(void) const { return _totalTrackNumber; }
int getDiscNumber(void) const { return _discNumber; }
int getTotalDiscNumber(void) const { return _totalDiscNumber; }
std::string getName(void) const { return _name; }
boost::filesystem::path getPath(void) const { return _filePath; }
boost::posix_time::time_duration getDuration(void) const { return _duration; }
boost::posix_time::ptime getDate(void) const { return _date; }
boost::posix_time::ptime getOriginalDate(void) const { return _originalDate; }
boost::posix_time::ptime getLastWriteTime(void) const { return _fileLastWrite; }
boost::posix_time::ptime getAddedTime(void) const { return _fileAdded; }
const std::vector<unsigned char>& getChecksum(void) const { return _fileChecksum; }
CoverType getCoverType(void) const { return _coverType; }
const std::string& getMBID(void) const { return _MBID; }
Wt::Dbo::ptr<Artist> getArtist(void) const { return _artist; }
Wt::Dbo::ptr<Release> getRelease(void) const { return _release; }
std::vector< Genre::pointer > getGenres(void) const;
bool hasGenre(Genre::pointer genre) const { return _genres.count(genre); }
std::size_t getScanVersion() const { return _scanVersion; }
boost::optional<std::size_t> getTrackNumber() const;
boost::optional<std::size_t> getTotalTrackNumber() const;
boost::optional<std::size_t> getDiscNumber() const;
boost::optional<std::size_t> getTotalDiscNumber() const;
std::string getName() const { return _name; }
boost::filesystem::path getPath() const { return _filePath; }
std::chrono::milliseconds getDuration() const { return _duration; }
boost::optional<int> getYear() const;
boost::optional<int> getOriginalYear() const;
Wt::WDateTime getLastWriteTime() const { return _fileLastWrite; }
Wt::WDateTime getAddedTime() const { return _fileAdded; }
const std::vector<unsigned char>& getChecksum() const { return _fileChecksum; }
CoverType getCoverType() const { return _coverType; }
const std::string& getMBID() const { return _MBID; }
Wt::Dbo::ptr<Artist> getArtist() const { return _artist; }
Wt::Dbo::ptr<Release> getRelease() const { return _release; }
std::vector<Wt::Dbo::ptr<Cluster>> getClusters() const;
std::vector<std::vector<Wt::Dbo::ptr<Cluster>>> getClusterGroups(std::vector<Wt::Dbo::ptr<ClusterType>> clusterTypes, std::size_t size) const;
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _scanVersion, "scan_version");
Wt::Dbo::field(a, _trackNumber, "track_number");
Wt::Dbo::field(a, _totalTrackNumber, "total_track_number");
Wt::Dbo::field(a, _discNumber, "disc_number");
Wt::Dbo::field(a, _totalDiscNumber, "total_disc_number");
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::field(a, _duration, "duration");
Wt::Dbo::field(a, _date, "date");
Wt::Dbo::field(a, _originalDate, "original_date");
Wt::Dbo::field(a, _year, "year");
Wt::Dbo::field(a, _originalYear, "original_year");
Wt::Dbo::field(a, _genreList, "genre_list");
Wt::Dbo::field(a, _filePath, "file_path");
Wt::Dbo::field(a, _fileLastWrite, "file_last_write");
@@ -196,46 +142,40 @@ class Track
Wt::Dbo::field(a, _MBID, "mbid");
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _genres, Wt::Dbo::ManyToMany, "track_genre", "", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _playlistEntries, Wt::Dbo::ManyToOne, "track");
}
private:
static Wt::Dbo::Query< pointer > getQuery(Wt::Dbo::Session& session, SearchFilter filter);
static const std::size_t _maxNameLength = 128;
int _trackNumber;
int _totalTrackNumber;
int _discNumber;
int _totalDiscNumber;
int _scanVersion = 0;
int _trackNumber = 0;
int _totalTrackNumber = 0;
int _discNumber = 0;
int _totalDiscNumber = 0;
std::string _name;
std::string _artistName;
std::string _releaseName;
boost::posix_time::time_duration _duration;
boost::posix_time::ptime _date;
boost::posix_time::ptime _originalDate; // original date time
std::chrono::duration<int, std::milli> _duration;
int _year = 0;
int _originalYear = 0;
std::string _genreList;
std::string _filePath;
std::vector<unsigned char> _fileChecksum;
boost::posix_time::ptime _fileLastWrite;
boost::posix_time::ptime _fileAdded;
CoverType _coverType;
std::string _MBID; // Musicbrainz Identifier
Wt::WDateTime _fileLastWrite;
Wt::WDateTime _fileAdded;
CoverType _coverType = CoverType::None;
std::string _MBID = ""; // Musicbrainz Identifier
Wt::Dbo::ptr<Artist> _artist;
Wt::Dbo::ptr<Release> _release;
Wt::Dbo::collection< Genre::pointer > _genres; // Genres that are related to this track
Wt::Dbo::collection< Wt::Dbo::ptr<PlaylistEntry> > _playlistEntries;
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> _clusters;
Wt::Dbo::collection<Wt::Dbo::ptr<TrackListEntry>> _playlistEntries;
};
} // namespace database
#endif
+269
View File
@@ -0,0 +1,269 @@
/*
* Copyright (C) 2014 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "TrackList.hpp"
#include <cassert>
#include <random>
#include "utils/Logger.hpp"
#include "Artist.hpp"
#include "Cluster.hpp"
#include "Release.hpp"
#include "User.hpp"
#include "Track.hpp"
namespace Database {
TrackList::TrackList()
: _isPublic(false)
{
}
TrackList::TrackList(std::string name, bool isPublic, Wt::Dbo::ptr<User> user)
: _name(name),
_isPublic(isPublic),
_user(user)
{
}
TrackList::pointer
TrackList::create(Wt::Dbo::Session& session, std::string name, bool isPublic, Wt::Dbo::ptr<User> user)
{
assert(user);
auto res = session.add( std::make_unique<TrackList>(name, isPublic, user) );
session.flush();
return res;
}
TrackListEntry::pointer
TrackList::add(IdType trackId)
{
assert(session());
assert(self());
return TrackListEntry::create(*session(), Database::Track::getById(*session(), trackId), self());
}
TrackList::pointer
TrackList::get(Wt::Dbo::Session& session, std::string name, Wt::Dbo::ptr<User> user)
{
return session.find<TrackList>().where("name = ? AND user_id = ?").bind(name).bind(user.id());
}
std::vector<TrackList::pointer>
TrackList::getAll(Wt::Dbo::Session& session, Wt::Dbo::ptr<User> user)
{
Wt::Dbo::collection<TrackList::pointer> res = session.find<TrackList>().where("user_id = ?").bind(user.id()).orderBy("name");
return std::vector<TrackList::pointer>(res.begin(), res.end());
}
TrackList::pointer
TrackList::getById(Wt::Dbo::Session& session, IdType id)
{
return session.find<TrackList>().where("id = ?").bind(id);
}
std::vector<Wt::Dbo::ptr<TrackListEntry>>
TrackList::getEntries(int offset, int size) const
{
assert(session());
assert(IdIsValid(self()->id()));
Wt::Dbo::collection<Wt::Dbo::ptr<TrackListEntry>> entries =
session()->find<TrackListEntry>()
.where("tracklist_id = ?").bind(self().id())
.orderBy("id")
.limit(size)
.offset(offset);
return std::vector<Wt::Dbo::ptr<TrackListEntry>>(entries.begin(), entries.end());
}
std::vector<Wt::Dbo::ptr<TrackListEntry>>
TrackList::getEntriesReverse(int offset, int size) const
{
assert(session());
assert(IdIsValid(self()->id()));
Wt::Dbo::collection<Wt::Dbo::ptr<TrackListEntry>> entries =
session()->find<TrackListEntry>()
.where("tracklist_id = ?").bind(self().id())
.orderBy("id DESC")
.limit(size)
.offset(offset);
return std::vector<Wt::Dbo::ptr<TrackListEntry>>(entries.begin(), entries.end());
}
Wt::Dbo::ptr<TrackListEntry>
TrackList::getEntry(std::size_t pos) const
{
Wt::Dbo::ptr<TrackListEntry> res;
auto entries = getEntries(pos, 1);
if (!entries.empty())
res = entries.front();
return res;
}
std::size_t
TrackList::getCount() const
{
return _entries.size();
}
std::vector<Wt::Dbo::ptr<Cluster>>
TrackList::getClusters() const
{
assert(session());
assert(IdIsValid(self()->id()));
Wt::Dbo::collection<Cluster::pointer> res = session()->query<Cluster::pointer>("SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id")
.where("p.id = ?").bind(self()->id())
.groupBy("c.id")
.orderBy("COUNT(c.id) DESC");
return std::vector<Wt::Dbo::ptr<Cluster>>(res.begin(), res.end());
}
bool
TrackList::hasTrack(IdType trackId) const
{
assert(session());
assert(IdIsValid(self()->id()));
Wt::Dbo::collection<TrackListEntry::pointer> res = session()->query<TrackListEntry::pointer>("SELECT p_e from tracklist_entry p_e INNER JOIN tracklist p ON p_e.tracklist_id = p.id")
.where("p_e.track_id = ?").bind(trackId)
.where("p.id = ?").bind(self()->id());
return res.size() > 0;
}
std::vector<IdType>
TrackList::getTrackIds() const
{
assert(session());
assert(IdIsValid(self()->id()));
Wt::Dbo::collection<IdType> res = session()->query<IdType>("SELECT p_e.track_id from tracklist_entry p_e INNER JOIN tracklist p ON p_e.tracklist_id = p.id")
.where("p.id = ?").bind(self()->id());
return std::vector<IdType>(res.begin(), res.end());
}
void
TrackList::shuffle()
{
assert(session());
auto entries = getEntries();
auto now = std::chrono::system_clock::now();
std::mt19937 randGenerator(std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count());
std::shuffle(entries.begin(), entries.end(), randGenerator);
clear();
for (auto entry : entries)
TrackListEntry::create(*session(), entry->getTrack(), self());
}
std::vector<Artist::pointer>
TrackList::getTopArtists(int limit) const
{
assert(session());
assert(IdIsValid(self()->id()));
Wt::Dbo::collection<Artist::pointer> res = session()->query<Artist::pointer>("SELECT a from artist a INNER JOIN track t ON t.artist_id = a.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id")
.where("p.id = ?").bind(self()->id())
.groupBy("a.id")
.orderBy("COUNT(a.id) DESC")
.limit(limit);
return std::vector<Artist::pointer>(res.begin(), res.end());
}
std::vector<Release::pointer>
TrackList::getTopReleases(int limit) const
{
assert(session());
assert(IdIsValid(self()->id()));
Wt::Dbo::collection<Release::pointer> res = session()->query<Release::pointer>("SELECT r from release r INNER JOIN track t ON t.release_id = r.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id")
.where("p.id = ?").bind(self()->id())
.groupBy("r.id")
.orderBy("COUNT(r.id) DESC")
.limit(limit);
return std::vector<Release::pointer>(res.begin(), res.end());
}
std::vector<Track::pointer>
TrackList::getTopTracks(int limit) const
{
assert(session());
assert(IdIsValid(self()->id()));
Wt::Dbo::collection<Track::pointer> res = session()->query<Track::pointer>("SELECT t from track t INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id")
.where("p.id = ?").bind(self()->id())
.groupBy("t.id")
.orderBy("COUNT(t.id) DESC")
.limit(limit);
return std::vector<Track::pointer>(res.begin(), res.end());
}
TrackListEntry::TrackListEntry(Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<TrackList> tracklist)
: _track(track),
_tracklist(tracklist)
{
}
TrackListEntry::TrackListEntry()
{
}
TrackListEntry::pointer
TrackListEntry::create(Wt::Dbo::Session& session, Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<TrackList> tracklist)
{
assert(track);
assert(tracklist);
auto res = session.add( std::make_unique<TrackListEntry>( track, tracklist) );
session.flush();
return res;
}
TrackListEntry::pointer
TrackListEntry::getById(Wt::Dbo::Session& session, IdType id)
{
return session.find<TrackListEntry>().where("id = ?").bind(id);
}
} // namespace Database
+129
View File
@@ -0,0 +1,129 @@
/*
* Copyright (C) 2014 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <Wt/Dbo/Dbo.h>
#include <string>
#include "Types.hpp"
namespace Database {
class Artist;
class Release;
class User;
class Track;
class TrackListEntry;
class Cluster;
class TrackList : public Wt::Dbo::Dbo<TrackList>
{
public:
using pointer = Wt::Dbo::ptr<TrackList>;
TrackList();
TrackList(std::string name, bool isPublic, Wt::Dbo::ptr<User> user);
// Stats utility
std::vector<Wt::Dbo::ptr<Artist>> getTopArtists(int limit = 1) const;
std::vector<Wt::Dbo::ptr<Release>> getTopReleases(int limit = 1) const;
std::vector<Wt::Dbo::ptr<Track>> getTopTracks(int limit = 1) const;
// Search utility
static pointer get(Wt::Dbo::Session& session, std::string name, Wt::Dbo::ptr<User> user);
static pointer getById(Wt::Dbo::Session& session, IdType tracklistId);
static std::vector<pointer> getAll(Wt::Dbo::Session& session, Wt::Dbo::ptr<User> user);
// Create utility
static pointer create(Wt::Dbo::Session& session, std::string name, bool isPublic, Wt::Dbo::ptr<User> user);
// Accessors
std::string getName() const { return _name; }
bool isPublic() const { return _isPublic; }
// Modifiers
Wt::Dbo::ptr<TrackListEntry> add(IdType trackId);
void clear() { _entries.clear(); }
void shuffle();
// Get tracks, ordered by position
std::size_t getCount() const;
Wt::Dbo::ptr<TrackListEntry> getEntry(std::size_t pos) const;
std::vector<Wt::Dbo::ptr<TrackListEntry>> getEntries(int offset = -1, int size = -1) const;
std::vector<Wt::Dbo::ptr<TrackListEntry>> getEntriesReverse(int offset = -1, int size = -1) const;
std::vector<IdType> getTrackIds() const;
// Get clusters, order by occurence
std::vector<Wt::Dbo::ptr<Cluster>> getClusters() const;
bool hasTrack(IdType trackId) const;
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::field(a, _isPublic, "public");
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _entries, Wt::Dbo::ManyToOne, "tracklist");
}
private:
std::string _name;
bool _isPublic;
Wt::Dbo::ptr<User> _user;
Wt::Dbo::collection< Wt::Dbo::ptr<TrackListEntry> > _entries;
};
class TrackListEntry : public Wt::Dbo::Dbo<TrackListEntry>
{
public:
using pointer = Wt::Dbo::ptr<TrackListEntry>;
TrackListEntry();
TrackListEntry(Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<TrackList> tracklist);
static pointer getById(Wt::Dbo::Session& session, IdType id);
// Create utility
static pointer create(Wt::Dbo::Session& session, Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<TrackList> tracklist);
// Accessors
Wt::Dbo::ptr<Track> getTrack() const { return _track; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _tracklist, "tracklist", Wt::Dbo::OnDeleteCascade);
}
private:
Wt::Dbo::ptr<Track> _track;
Wt::Dbo::ptr<TrackList> _tracklist;
};
} // namespace Database
+11 -8
View File
@@ -17,13 +17,16 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
// header file aimed to ease database class declarations
#pragma once
#include "Artist.hpp"
#include "Track.hpp"
#include "Playlist.hpp"
#include "Release.hpp"
#include "Video.hpp"
#include "MediaDirectory.hpp"
#include "User.hpp"
#include <Wt/Dbo/Dbo.h>
namespace Database {
using IdType = Wt::Dbo::dbo_default_traits::IdType;
static inline bool IdIsValid(IdType id)
{
return id != Wt::Dbo::dbo_default_traits::invalidId();
}
}
+52 -81
View File
@@ -17,7 +17,9 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Types.hpp"
#include "User.hpp"
#include "TrackList.hpp"
namespace Database {
@@ -26,51 +28,16 @@ const std::vector<std::size_t>
User::audioBitrates =
{
64000,
96000,
128000,
160000,
192000,
224000,
256000,
320000,
512000
};
const std::vector<std::size_t>
User::videoBitrates =
{
256000,
512000,
1024000,
2048000,
4096000,
8192000
};
const std::vector<AudioEncoding>
User::audioEncodings =
{
AudioEncoding::AUTO,
AudioEncoding::MP3,
AudioEncoding::OGA,
AudioEncoding::WEBMA,
};
const std::vector<VideoEncoding>
User::videoEncodings =
{
VideoEncoding::AUTO,
};
User::User()
: _maxAudioBitrate(maxAudioBitrate),
_maxVideoBitrate(maxVideoBitrate),
_isAdmin(false),
: _maxAudioBitrate(audioBitrates.back()),
_type(Type::REGULAR),
_audioBitrate(defaultAudioBitrate),
_audioEncoding(AudioEncoding::AUTO),
_videoBitrate(defaultVideoBitrate),
_videoEncoding(VideoEncoding::AUTO),
_curPlayingTrackPos(0)
{
@@ -84,76 +51,80 @@ User::getAll(Wt::Dbo::Session& session)
}
User::pointer
User::getById(Wt::Dbo::Session& session, std::string id)
User::getDemo(Wt::Dbo::Session& session)
{
return session.find<User>().where("id = ?").bind( id );
pointer res = session.find<User>().where("type = ?").bind(Type::DEMO);
return res;
}
std::string
User::getId( pointer user)
User::pointer
User::create(Wt::Dbo::Session& session)
{
std::ostringstream oss; oss << user.id();
return oss.str();
return session.add(std::make_unique<User>());
}
User::pointer
User::getById(Wt::Dbo::Session& session, IdType id)
{
return session.find<User>().where("id = ?").bind( id );
}
void
User::setAudioBitrate(std::size_t bitrate)
{
_audioBitrate = std::min(bitrate, std::min(static_cast<std::size_t>(_maxAudioBitrate), audioBitrates.back()));
}
void
User::setVideoBitrate(std::size_t bitrate)
{
_videoBitrate = std::min(bitrate, std::min(static_cast<std::size_t>(_maxVideoBitrate), videoBitrates.back()));
_audioBitrate = std::min(bitrate, static_cast<std::size_t>(_maxAudioBitrate));
}
void
User::setMaxAudioBitrate(std::size_t bitrate)
{
_maxAudioBitrate = std::min(bitrate, static_cast<std::size_t>(_maxAudioBitrate));
}
void
User::setMaxVideoBitrate(std::size_t bitrate)
{
_maxVideoBitrate = std::min(bitrate, static_cast<std::size_t>(_maxVideoBitrate));
_maxAudioBitrate = std::min(bitrate, audioBitrates.back());
if (_audioBitrate > _maxAudioBitrate)
_audioBitrate = _maxAudioBitrate;
}
std::size_t
User::getAudioBitrate(void) const
{
if (!isAdmin())
return std::min(static_cast<std::size_t>(_audioBitrate), std::min(static_cast<std::size_t>(_maxAudioBitrate), audioBitrates.back()));
else
return std::min(static_cast<std::size_t>(_audioBitrate), audioBitrates.back());
}
std::size_t
User::getVideoBitrate(void) const
{
if (!isAdmin())
return std::min(static_cast<std::size_t>(_videoBitrate), std::min(static_cast<std::size_t>(_maxVideoBitrate), videoBitrates.back()));
else
return std::min(static_cast<std::size_t>(_videoBitrate), videoBitrates.back());
return _audioBitrate;
}
std::size_t
User::getMaxAudioBitrate(void) const
{
if (!isAdmin())
return std::min(static_cast<std::size_t>(_maxAudioBitrate), audioBitrates.back());
else
return audioBitrates.back();
return _maxAudioBitrate;
}
std::size_t
User::getMaxVideoBitrate(void) const
Wt::Dbo::ptr<TrackList>
User::getPlayedTrackList() const
{
if (!isAdmin())
return std::min(static_cast<std::size_t>(_maxVideoBitrate), videoBitrates.back());
else
return videoBitrates.back();
static const std::string listName = "__played_tracks__";
assert(self());
assert(IdIsValid(self()->id()));
assert(session());
auto res = TrackList::get(*session(), listName, self());
if (!res)
res = TrackList::create(*session(), listName, false, self());
return res;
}
Wt::Dbo::ptr<TrackList>
User::getQueuedTrackList() const
{
static const std::string listName = "__queued_tracks__";
assert(self());
assert(IdIsValid(self()->id()));
assert(session());
auto res = TrackList::get(*session(), listName, self());
if (!res)
res = TrackList::create(*session(), listName, false, self());
return res;
}
} // namespace Database
+33 -45
View File
@@ -17,20 +17,21 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef DATABASE_USER_HPP
#define DATABASE_USER_HPP
#pragma once
#include <vector>
#include <Wt/Dbo/Dbo>
#include <Wt/Auth/Dbo/AuthInfo>
#include <Wt/Dbo/Dbo.h>
#include <Wt/Auth/Dbo/AuthInfo.h>
#include "Types.hpp"
namespace Database {
class User;
typedef Wt::Auth::Dbo::AuthInfo<User> AuthInfo;
using AuthInfo = Wt::Auth::Dbo::AuthInfo<User>;
class Playlist;
class TrackList;
// User selectable audio formats
enum class AudioEncoding
@@ -41,95 +42,82 @@ enum class AudioEncoding
WEBMA,
};
enum class VideoEncoding
class User : public Wt::Dbo::Dbo<User>
{
AUTO,
};
class User
{
public:
using pointer = Wt::Dbo::ptr<User>;
static const std::size_t MinNameLength = 3;
static const std::size_t MaxNameLength = 15;
// list of audio/video parameters
static const std::vector<std::size_t> audioBitrates;
static const std::vector<AudioEncoding> audioEncodings;
enum class Type
{
REGULAR,
ADMIN,
DEMO
};
static const std::vector<std::size_t> videoBitrates;
static const std::vector<VideoEncoding> videoEncodings;
// list of audio parameters
static const std::vector<std::size_t> audioBitrates;
User();
typedef Wt::Dbo::ptr<User> pointer;
// utility
static pointer create(Wt::Dbo::Session& session);
// accessors
static pointer getById(Wt::Dbo::Session& session, std::string id);
static pointer getById(Wt::Dbo::Session& session, IdType id);
static std::vector<pointer> getAll(Wt::Dbo::Session& session);
static std::string getId(pointer user);
static pointer getDemo(Wt::Dbo::Session& session);
// write
void setAdmin(bool admin) { _isAdmin = admin; }
void setType(Type type) { _type = type; }
void setAudioBitrate(std::size_t bitrate);
void setAudioEncoding(AudioEncoding encoding) { _audioEncoding = encoding; }
void setVideoBitrate(std::size_t bitrate);
void setVideoEncoding(VideoEncoding encoding) { _videoEncoding = encoding; }
void setMaxAudioBitrate(std::size_t bitrate);
void setMaxVideoBitrate(std::size_t bitrate);
void setCurPlayingTrackPos(std::size_t pos) { _curPlayingTrackPos = pos; }
// read
bool isAdmin() const {return _isAdmin;}
bool isAdmin() const { return _type == Type::ADMIN; }
bool isDemo() const { return _type == Type::DEMO; }
std::size_t getAudioBitrate() const;
AudioEncoding getAudioEncoding() const { return _audioEncoding;}
std::size_t getVideoBitrate() const;
VideoEncoding getVideoEncoding() const { return _videoEncoding;}
AudioEncoding getAudioEncoding() const { return _audioEncoding; }
std::size_t getMaxAudioBitrate() const;
std::size_t getMaxVideoBitrate() const;
std::size_t getCurPlayingTrackPos() const { return _curPlayingTrackPos; }
Wt::Dbo::ptr<TrackList> getQueuedTrackList() const;
Wt::Dbo::ptr<TrackList> getPlayedTrackList() const;
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _maxAudioBitrate, "max_audio_bitrate");
Wt::Dbo::field(a, _maxVideoBitrate, "max_video_bitrate");
Wt::Dbo::field(a, _isAdmin, "admin");
Wt::Dbo::field(a, _type, "type");
Wt::Dbo::field(a, _audioBitrate, "audio_bitrate");
Wt::Dbo::field(a, _audioEncoding, "audio_encoding");
Wt::Dbo::field(a, _videoBitrate, "video_bitrate");
Wt::Dbo::field(a, _videoEncoding, "video_encoding");
// User's dynamic data
Wt::Dbo::field(a, _curPlayingTrackPos, "cur_playing_track_pos");
Wt::Dbo::hasMany(a, _playlists, Wt::Dbo::ManyToOne, "user");
Wt::Dbo::hasMany(a, _tracklists, Wt::Dbo::ManyToOne, "user");
}
private:
static const std::size_t maxAudioBitrate = 320000;
static const std::size_t maxVideoBitrate = 2048000;
static const std::size_t defaultAudioBitrate = 128000;
static const std::size_t defaultVideoBitrate = 1024000;
// Admin defined settings
int _maxAudioBitrate;
int _maxVideoBitrate;
bool _isAdmin;
Type _type;
// User defined settings
int _audioBitrate;
AudioEncoding _audioEncoding;
int _videoBitrate;
VideoEncoding _videoEncoding;
// User's dynamic data
int _curPlayingTrackPos; // Current track position in queue
Wt::Dbo::collection< Wt::Dbo::ptr<Playlist> > _playlists;
Wt::Dbo::collection< Wt::Dbo::ptr<TrackList> > _tracklists;
};
} // namespace Databas'
#endif
-59
View File
@@ -1,59 +0,0 @@
/*
* Copyright (C) 2013 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Types.hpp"
namespace Database {
Video::Video()
{
}
Video::Video(const boost::filesystem::path& p)
: _filePath(p.string())
{
}
Video::pointer
Video::create(Wt::Dbo::Session& session, const boost::filesystem::path& p)
{
return session.add(new Video(p));
}
Wt::Dbo::collection< Video::pointer >
Video::getAll(Wt::Dbo::Session& session)
{
return session.find<Video>();
}
Video::pointer
Video::getByPath(Wt::Dbo::Session& session, const boost::filesystem::path& p)
{
return session.find<Video>().where("path = ?").bind(p.string());
}
std::vector<boost::filesystem::path>
Video::getAllPaths(Wt::Dbo::Session& session)
{
Wt::Dbo::Transaction transaction(session);
Wt::Dbo::collection<std::string> res = session.query<std::string>("SELECT path from video");
return std::vector<boost::filesystem::path>(res.begin(), res.end());
}
} // namespace Video
-84
View File
@@ -1,84 +0,0 @@
/*
* Copyright (C) 2013 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _VIDEO_TYPES_HPP
#define _VIDEO_TYPES_HPP
#include <string>
#include <boost/filesystem.hpp>
#include <Wt/Dbo/Dbo>
#include <Wt/Dbo/WtSqlTraits>
#include <Wt/WDateTime>
namespace Database {
class Video
{
public:
typedef Wt::Dbo::ptr<Video> pointer;
Video();
Video( const boost::filesystem::path& p);
// Find utilities
static pointer getByPath(Wt::Dbo::Session& session, const boost::filesystem::path& p);
static Wt::Dbo::collection< pointer > getAll(Wt::Dbo::Session& session);
static std::vector<boost::filesystem::path> getAllPaths(Wt::Dbo::Session& session);
// Create utility
static pointer create(Wt::Dbo::Session& session, const boost::filesystem::path& p);
// Modifiers
void setName(const std::string& name) { _name = name; }
void setDuration(boost::posix_time::time_duration duration) { _duration = duration; }
void setLastWriteTime(boost::posix_time::ptime time) { _fileLastWrite = time; }
// Accessors
std::string getName(void) const { return _name; }
boost::posix_time::time_duration getDuration(void) const { return _duration; }
boost::filesystem::path getPath(void) const { return _filePath; }
boost::posix_time::ptime getLastWriteTime(void) const { return _fileLastWrite; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::field(a, _duration, "duration");
Wt::Dbo::field(a, _fileLastWrite, "last_write");
Wt::Dbo::field(a, _filePath, "path");
}
private:
std::string _name;
boost::posix_time::time_duration _duration;
std::string _filePath;
boost::posix_time::ptime _fileLastWrite;
}; // Video
} // namespace Database
#endif
+2 -2
View File
@@ -16,10 +16,10 @@
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "logger/Logger.hpp"
#include "Image.hpp"
#include "utils/Logger.hpp"
namespace Image {
@@ -78,7 +78,7 @@ Image::load(boost::filesystem::path p)
}
catch (Magick::Exception& e)
{
LMS_LOG(COVER, ERROR) << "Caught Magick exception while loading image from file '" << p << "': " << e.what();
LMS_LOG(COVER, ERROR) << "Caught Magick exception while loading image from file '" << p.string() << "': " << e.what();
return false;
}
}
+100 -28
View File
@@ -18,38 +18,100 @@
*/
#include <boost/filesystem.hpp>
#include <boost/property_tree/xml_parser.hpp>
#include "config/config.h"
#include <Wt/WServer.h>
#include <Wt/WApplication.h>
#include "utils/Config.hpp"
#include "utils/Logger.hpp"
#include "av/AvInfo.hpp"
#include "av/AvTranscoder.hpp"
#include "logger/Logger.hpp"
#include "image/Image.hpp"
#include "scanner/MediaScanner.hpp"
#include "ui/LmsApplication.hpp"
#include "service/ServiceManager.hpp"
#include "service/DatabaseUpdateService.hpp"
std::vector<std::string> generateWtConfig(std::string execPath)
{
std::vector<std::string> args;
boost::filesystem::path wtConfigPath = Config::instance().getPath("working-dir") / "wt_config.xml";
boost::filesystem::path wtLogFilePath = Config::instance().getPath("working-dir") / "lms.log";
args.push_back(execPath);
args.push_back("--config=" + wtConfigPath.string());
args.push_back("--docroot=" + Config::instance().getString("docroot"));
args.push_back("--approot=" + Config::instance().getString("approot"));
args.push_back("--resources-dir=" + Config::instance().getString("wt-resources"));
if (Config::instance().getBool("tls-enable", false))
{
args.push_back("--https-port=" + std::to_string( Config::instance().getULong("listen-port", 5081)));
args.push_back("--https-address=" + Config::instance().getString("listen-addr", "0.0.0.0"));
args.push_back("--ssl-certificate=" + Config::instance().getString("tls-cert"));
args.push_back("--ssl-private-key=" + Config::instance().getString("tls-key"));
args.push_back("--ssl-tmp-dh=" + Config::instance().getString("tls-dh"));
}
else
{
args.push_back("--http-port=" + std::to_string( Config::instance().getULong("listen-port", 5081)));
args.push_back("--http-address=" + Config::instance().getString("listen-addr", "0.0.0.0"));
}
// Generate the wt_config.xml file
boost::property_tree::ptree pt;
pt.put("server.application-settings.<xmlattr>.location", "*");
pt.put("server.application-settings.log-file", wtLogFilePath.string());
pt.put("server.application-settings.behind-reverse-proxy", Config::instance().getBool("behind-reverse-proxy", false));
pt.put("server.application-settings.progressive-bootstrap", true);
std::ofstream oss(wtConfigPath.string().c_str(), std::ios::out);
boost::property_tree::xml_parser::write_xml(oss, pt);
return args;
}
#include <Wt/WServer>
int main(int argc, char* argv[])
{
boost::filesystem::path configFilePath = "/etc/lms.conf";
int res = EXIT_FAILURE;
assert(argc > 0);
assert(argv[0] != NULL);
if (argc >= 2)
configFilePath = std::string(argv[1], 0, 256);
try
{
// Make pstream work with ffmpeg
close(STDIN_FILENO);
Config::instance().setFile(configFilePath);
// Make sure the working directory exists
// TODO check with boost::system::error_code ec;
boost::filesystem::create_directories(Config::instance().getPath("working-dir"));
boost::filesystem::create_directories(Config::instance().getPath("working-dir") / "features");
// Construct WT configuration and get the argc/argv back
std::vector<std::string> wtServerArgs = generateWtConfig(argv[0]);
const char* wtArgv[wtServerArgs.size()];
for (std::size_t i = 0; i < wtServerArgs.size(); ++i)
{
std::cout << "ARG = " << wtServerArgs[i] << std::endl;
wtArgv[i] = wtServerArgs[i].c_str();
}
Wt::WServer server(argv[0]);
server.setServerConfiguration (argc, argv);
server.setServerConfiguration (wtServerArgs.size(), const_cast<char**>(wtArgv));
Wt::WServer::instance()->logger().configure("*"); // log everything
Service::ServiceManager& serviceManager = Service::ServiceManager::instance();
Wt::WServer::instance()->logger().configure("*"); // log everything, TODO configure this
// lib init
Image::init(argv[0]);
@@ -58,42 +120,52 @@ int main(int argc, char* argv[])
Database::Handler::configureAuth();
// Initializing a connection pool to the database that will be shared along services
std::unique_ptr<Wt::Dbo::SqlConnectionPool> connectionPool( Database::Handler::createConnectionPool("/var/lms/lms.db")); // TODO use $datadir from autotools
auto connectionPool = Database::Handler::createConnectionPool(Config::instance().getPath("working-dir") / "lms.db");
serviceManager.add( std::make_shared<Service::DatabaseUpdateService>(*connectionPool));
UserInterface::LmsApplicationGroupContainer appGroups;
Scanner::MediaScanner scanner(*connectionPool);
// bind entry point
server.addEntryPoint(Wt::Application, boost::bind(UserInterface::LmsApplication::create, _1, boost::ref(*connectionPool)));
server.addEntryPoint(Wt::EntryPointType::Application,
std::bind(UserInterface::LmsApplication::create,
std::placeholders::_1, std::ref(*connectionPool), std::ref(appGroups), std::ref(scanner)));
// Start
LMS_LOG(MAIN, INFO) << "Starting Media scanner...";
scanner.start();
// Starting the main server
LMS_LOG(MAIN, INFO) << "Starting server...";
server.start();
// Start underlying services
LMS_LOG(MAIN, INFO) << "Starting services...";
serviceManager.start();
// Wait
LMS_LOG(MAIN, INFO) << "Now running...";
Wt::WServer::waitForShutdown();
// Waiting for shutdown command
Wt::WServer::waitForShutdown(argv[0]);
LMS_LOG(MAIN, INFO) << "Stopping services...";
serviceManager.stop();
serviceManager.clear();
// Stop
LMS_LOG(MAIN, INFO) << "Stopping server...";
server.stop();
LMS_LOG(MAIN, INFO) << "Stopping database updater...";
scanner.stop();
LMS_LOG(MAIN, INFO) << "Clean stop!";
res = EXIT_SUCCESS;
}
catch( Wt::WServer::Exception& e)
catch( libconfig::FileIOException& e)
{
std::cerr << "Caught a WServer::Exception: " << e.what();
std::cerr << "Cannot open config file '" << configFilePath << "'" << std::endl;
}
catch( std::exception& e)
catch( libconfig::ParseException& e)
{
std::cerr << "Caught std::exception: " << e.what();
std::cerr << "Caught libconfig::ParseException! error='" << e.getError() << "', file = '" << e.getFile() << "', line = " << e.getLine() << std::endl;
}
catch(Wt::WServer::Exception& e)
{
std::cerr << "Caught a WServer::Exception: " << e.what() << std::endl;
}
catch(std::exception& e)
{
std::cerr << "Caught std::exception: " << e.what() << std::endl;
}
return res;
+116 -139
View File
@@ -19,181 +19,158 @@
#include "AvFormat.hpp"
#include "logger/Logger.hpp"
#include "utils/Utils.hpp"
#include <boost/algorithm/string.hpp>
#include "av/AvInfo.hpp"
#include "utils/Logger.hpp"
#include "utils/Utils.hpp"
namespace MetaData
{
bool
AvFormat::parse(const boost::filesystem::path& p, Items& items)
boost::optional<Items>
AvFormat::parse(const boost::filesystem::path& p, bool debug)
{
Items items;
Av::MediaFile mediaFile(p);
if (!mediaFile.open())
return false;
if (!mediaFile.scan())
return false;
std::map<std::string, std::string> metadata = mediaFile.getMetaData();
// Stream info
try
{
std::vector<AudioStream> audioStreams;
Av::MediaFile mediaFile(p);
std::vector<Av::Stream> streams = mediaFile.getStreams(Av::Stream::Type::Audio);
for (Av::Stream& stream : streams)
// Stream info
{
AudioStream audioStream;
audioStream.desc = stream.desc;
audioStream.bitRate = stream.bitrate;
std::vector<AudioStream> audioStreams;
audioStreams.push_back(audioStream);
auto streams = mediaFile.getStreamInfo();
for (auto stream : streams)
audioStreams.push_back( {.bitRate = stream.bitrate } );
if (!audioStreams.empty())
items.insert( std::make_pair(MetaData::Type::AudioStreams, audioStreams));
}
if (!audioStreams.empty())
items.insert( std::make_pair(MetaData::Type::AudioStreams, audioStreams));
}
// Duration
items.insert( std::make_pair(MetaData::Type::Duration, mediaFile.getDuration() ));
{
std::vector<VideoStream> videoStreams;
// Cover
items.insert( std::make_pair(MetaData::Type::HasCover, mediaFile.hasAttachedPictures()));
std::vector<Av::Stream> streams = mediaFile.getStreams(Av::Stream::Type::Video);
// Embedded MetaData
// Make sure to convert strings into UTF-8
for (Av::Stream& stream : streams)
MetaData::Clusters clusters;
std::map<std::string, std::string> metadataMap = mediaFile.getMetaData();
for (auto metadata : metadataMap)
{
VideoStream videoStream;
videoStream.desc = stream.desc;
videoStream.bitRate = stream.bitrate;
const std::string tag = boost::to_upper_copy<std::string>(metadata.first);
const std::string value = metadata.second;
videoStreams.push_back(videoStream);
}
if (debug)
std::cout << "TAG = " << tag << ", VAL = " << value << std::endl;
if (!videoStreams.empty())
items.insert( std::make_pair(MetaData::Type::VideoStreams, videoStreams));
}
{
std::vector<SubtitleStream> subtitleStreams;
std::vector<Av::Stream> streams = mediaFile.getStreams(Av::Stream::Type::Subtitle);
for (Av::Stream& stream : streams)
{
SubtitleStream subtitleStream;
subtitleStream.desc = stream.desc;
subtitleStreams.push_back(subtitleStream);
}
if (!subtitleStreams.empty())
items.insert( std::make_pair(MetaData::Type::SubtitleStreams, subtitleStreams));
}
// Duration
items.insert( std::make_pair(MetaData::Type::Duration, mediaFile.getDuration() ));
// Cover
items.insert( std::make_pair(MetaData::Type::HasCover, mediaFile.hasAttachedPictures()));
// Embedded MetaData
// Make sure to convert strings into UTF-8
std::map<std::string, std::string>::const_iterator it;
for (it = metadata.begin(); it != metadata.end(); ++it)
{
if (boost::iequals(it->first, "artist"))
items.insert( std::make_pair(MetaData::Type::Artist, stringTrim( stringToUTF8(it->second)) ));
else if (boost::iequals(it->first, "album"))
items.insert( std::make_pair(MetaData::Type::Album, stringTrim( stringToUTF8(it->second)) ));
else if (boost::iequals(it->first, "title"))
items.insert( std::make_pair(MetaData::Type::Title, stringTrim( stringToUTF8(it->second)) ));
else if (boost::iequals(it->first, "track"))
{
// Expecting 'Number/Total'
auto strings = splitString(it->second, "/");
if (strings.size() > 0)
if (tag == "ARTIST")
items.insert( std::make_pair(MetaData::Type::Artist, stringTrim( value) ));
else if (tag == "ALBUM")
items.insert( std::make_pair(MetaData::Type::Album, stringTrim( value) ));
else if (tag == "TITLE")
items.insert( std::make_pair(MetaData::Type::Title, stringTrim( value) ));
else if (tag == "TRACK")
{
std::size_t number;
if (readAs<std::size_t>(strings[0], number))
items.insert( std::make_pair(MetaData::Type::TrackNumber, number ));
// Expecting 'Number/Total'
auto strings = splitString(value, "/");
if (strings.size() > 1)
if (strings.size() > 0)
{
std::size_t totalNumber;
if (readAs<std::size_t>(strings[1], totalNumber))
items.insert( std::make_pair(MetaData::Type::TotalTrack, totalNumber ));
auto number = readAs<std::size_t>(strings[0]);
if (number)
items.insert( std::make_pair(MetaData::Type::TrackNumber, *number ));
if (strings.size() > 1)
{
auto totalNumber = readAs<std::size_t>(strings[1]);
if (totalNumber)
items.insert( std::make_pair(MetaData::Type::TotalTrack, *totalNumber ));
}
}
}
}
else if (boost::iequals(it->first, "disc"))
{
// Expecting 'Number/Total'
auto strings = splitString(it->second, "/");
if (strings.size() > 0)
else if (tag == "DISC")
{
std::size_t number;
if (readAs<std::size_t>(strings[0], number))
items.insert( std::make_pair(MetaData::Type::DiscNumber, number ));
// Expecting 'Number/Total'
auto strings = splitString(value, "/");
if (strings.size() > 1)
if (strings.size() > 0)
{
std::size_t totalNumber;
if (readAs<std::size_t>(strings[1], totalNumber))
items.insert( std::make_pair(MetaData::Type::TotalDisc, totalNumber ));
auto number = readAs<std::size_t>(strings[0]);
if (number)
items.insert( std::make_pair(MetaData::Type::DiscNumber, *number ));
if (strings.size() > 1)
{
auto totalNumber = readAs<std::size_t>(strings[1]);
if (totalNumber)
items.insert( std::make_pair(MetaData::Type::TotalDisc, *totalNumber ));
}
}
}
else if (tag == "DATE"
|| tag == "YEAR"
|| tag == "WM/Year")
{
auto date = readAs<int>(value);
if (date)
items.insert(std::make_pair(MetaData::Type::Year, *date));
}
else if (tag == "TDOR" // Original release time (ID3v2 2.4)
|| tag == "TORY") // Original release year
{
auto date = readAs<int>(value);
if (date)
items.insert(std::make_pair(MetaData::Type::OriginalYear, *date));
}
else if (tag == "MUSICBRAINZ ARTIST ID"
|| tag == "MUSICBRAINZ_ARTISTID")
{
items.insert( std::make_pair(MetaData::Type::MusicBrainzArtistID, stringTrim(value)) );
}
else if (tag == "MUSICBRAINZ ALBUM ID"
|| tag == "MUSICBRAINZ_ALBUMID")
{
items.insert( std::make_pair(MetaData::Type::MusicBrainzAlbumID, stringTrim(value)) );
}
else if (tag == "MUSICBRAINZ RELEASE TRACK ID"
|| tag == "MUSICBRAINZ_RELEASETRACKID"
|| tag == "MUSICBRAINZ_TRACKID")
{
items.insert( std::make_pair(MetaData::Type::MusicBrainzTrackID, stringTrim(value)) );
}
else if (tag == "ACOUSTID ID")
{
items.insert( std::make_pair(MetaData::Type::AcoustID, stringTrim(value)) );
}
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
{
std::vector<std::string> clusterNames = splitString(value, ";,\\");
if (!clusterNames.empty())
{
clusters[tag] = std::set<std::string>(clusterNames.begin(), clusterNames.end());
}
}
}
else if (boost::iequals(it->first, "date")
|| boost::iequals(it->first, "year")
|| boost::iequals(it->first, "WM/Year"))
{
boost::posix_time::ptime p;
if (readAsPosixTime(it->second, p))
items.insert( std::make_pair(MetaData::Type::Date, p));
}
else if (boost::iequals(it->first, "TDOR") // Original release time (ID3v2 2.4)
|| boost::iequals(it->first, "TORY")) // Original release year
{
boost::posix_time::ptime p;
if (readAsPosixTime(it->second, p))
items.insert( std::make_pair(MetaData::Type::OriginalDate, p));
}
else if (boost::iequals(it->first, "genre"))
{
// TODO use splitStrings
std::list<std::string> genres;
if (readList(it->second, ";,\\", genres))
items.insert( std::make_pair(MetaData::Type::Genres, genres));
}
else if (boost::iequals(it->first, "MusicBrainz Artist Id")
|| boost::iequals(it->first, "MUSICBRAINZ_ARTISTID"))
{
items.insert( std::make_pair(MetaData::Type::MusicBrainzArtistID, stringTrim( stringToUTF8(it->second)) ));
}
else if (boost::iequals(it->first, "MusicBrainz Album Id")
|| boost::iequals(it->first, "MUSICBRAINZ_ALBUMID"))
{
items.insert( std::make_pair(MetaData::Type::MusicBrainzAlbumID, stringTrim( stringToUTF8(it->second)) ));
}
else if (boost::iequals(it->first, "MusicBrainz Release Track Id")
|| boost::iequals(it->first, "MUSICBRAINZ_RELEASETRACKID")
|| boost::iequals(it->first, "MUSICBRAINZ_TRACKID"))
{
items.insert( std::make_pair(MetaData::Type::MusicBrainzTrackID, stringTrim( stringToUTF8(it->second)) ));
}
if (!clusters.empty())
items.insert( std::make_pair(MetaData::Type::Clusters, clusters) );
}
catch(Av::MediaFileException& e)
{
return items;
}
return true;
return items;
}
} // namespace MetaData
+6 -10
View File
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2013 Emeric Poupon
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
@@ -17,8 +17,10 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef METADATA_AVFORMAT_HPP
#define METADATA_AVFORMAT_HPP
#pragma once
#include <map>
#include <string>
#include "MetaData.hpp"
@@ -29,14 +31,8 @@ namespace MetaData
class AvFormat : public Parser
{
public:
bool parse(const boost::filesystem::path& p, Items& items);
private:
boost::optional<Items> parse(const boost::filesystem::path& p, bool debug = false) override;
};
} // namespace MetaData
#endif
+18 -27
View File
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2013 Emeric Poupon
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
@@ -17,12 +17,13 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef METADATA_HPP
#define METADATA_HPP
#pragma once
#include <map>
#include <set>
#include <boost/any.hpp>
#include <boost/optional.hpp>
#include <boost/filesystem.hpp>
namespace MetaData
@@ -30,59 +31,49 @@ namespace MetaData
enum class Type
{
// Name Type of the value
Artist, // string
Title, // string
Album, // string
Genres, // list<string>
Duration, // boost::posix_time::time_duration
Clusters, // Clusters, ex: { "genre", {"death metal", "brutal death"} }, { "albumgrouping", {"metal"} }
Duration, // std::chrono::milliseconds
TrackNumber, // size_t
DiscNumber, // size_t
TotalTrack, // size_t
TotalDisc, // size_t
Date, // boost::posix_time::ptime
OriginalDate, // boost::posix_time::ptime
Year, // int
OriginalYear, // int
HasCover, // bool
AudioStreams, // vector<AudioStream>
VideoStreams, // vector<VideoStream>
SubtitleStreams, // vector<SubtitleStream>
MusicBrainzArtistID, // string
MusicBrainzAlbumID, // string
MusicBrainzTrackID, // string
MusicBrainzRecordingID, // string
AcoustID, // string
};
// Used by Streams
struct AudioStream
{
std::string desc;
// TODO codec?
std::size_t bitRate;
};
struct VideoStream
{
std::string desc;
std::size_t bitRate;
};
struct SubtitleStream
{
std::string desc;
};
// Type and associated data
// See enum Type's comments
typedef std::map<Type, boost::any> Items;
using Items = std::map<Type, boost::any>;
using Clusters = std::map<std::string /* type */, std::set<std::string> /* names */>;
class Parser
{
public:
virtual boost::optional<Items> parse(const boost::filesystem::path& p, bool debug = false) = 0;
typedef std::shared_ptr<Parser> pointer;
virtual bool parse(const boost::filesystem::path& p, Items& items) = 0;
void setClusterTypeNames(const std::set<std::string>& clusterTypeNames) { _clusterTypeNames = clusterTypeNames; }
protected:
std::set<std::string> _clusterTypeNames;
};
} // namespace MetaData
#endif
+216
View File
@@ -0,0 +1,216 @@
/*
* Copyright (C) 2016 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "TagLibParser.hpp"
#include <taglib/fileref.h>
#include <taglib/tag.h>
#include <taglib/tpropertymap.h>
#include <taglib/mpegfile.h>
#include <taglib/id3v2tag.h>
#include "utils/Logger.hpp"
#include "utils/Utils.hpp"
namespace MetaData
{
boost::optional<Items>
TagLibParser::parse(const boost::filesystem::path& p, bool debug)
{
TagLib::FileRef f(p.string().c_str(),
true, // read audio properties
TagLib::AudioProperties::Average);
if (f.isNull())
return boost::none;
if (!f.audioProperties())
return boost::none;
Items items;
{
TagLib::AudioProperties *properties = f.audioProperties();
std::chrono::milliseconds duration(properties->length() * 1000);
items.insert( std::make_pair(MetaData::Type::Duration, duration) );
MetaData::AudioStream audioStream = { .bitRate = static_cast<std::size_t>(properties->bitrate() * 1000) };
items.insert( std::make_pair(MetaData::Type::AudioStreams, std::vector<MetaData::AudioStream>(1, audioStream ) ));
}
// Not that good embedded pictures handling
// MP3
if (TagLib::MPEG::File *mp3File = dynamic_cast<TagLib::MPEG::File*>(f.file()))
{
if (mp3File->ID3v2Tag())
{
if (!mp3File->ID3v2Tag()->frameListMap()["APIC"].isEmpty())
items.insert( std::make_pair(MetaData::Type::HasCover, true));
}
}
if (f.tag())
{
MetaData::Clusters clusters;
TagLib::PropertyMap properties = f.file()->properties();
for(auto property : properties)
{
const std::string tag = property.first.upper().to8Bit(true);
const TagLib::StringList& values = property.second;
if (tag.empty() || values.isEmpty() || values.front().isEmpty())
continue;
// TODO validate MBID format
if (debug)
{
std::vector<std::string> strs;
for (auto value : values)
strs.push_back(values.front().to8Bit(true));
std::cout << "[" << tag << "] = " << joinStrings(strs, ",") << std::endl;
}
if (tag == "ARTIST")
items.insert( std::make_pair(MetaData::Type::Artist, stringTrim( values.front().to8Bit(true))));
else if (tag == "ALBUM")
items.insert( std::make_pair(MetaData::Type::Album, stringTrim( values.front().to8Bit(true))));
else if (tag == "TITLE")
items.insert( std::make_pair(MetaData::Type::Title, stringTrim( values.front().to8Bit(true))));
else if (tag == "MUSICBRAINZ_RELEASETRACKID"
|| tag == "MUSICBRAINZ RELEASE TRACK ID")
{
items.insert( std::make_pair(MetaData::Type::MusicBrainzTrackID, stringTrim( values.front().to8Bit(true))));
}
else if (tag == "MUSICBRAINZ_ARTISTID")
items.insert( std::make_pair(MetaData::Type::MusicBrainzArtistID, stringTrim( values.front().to8Bit(true))));
else if (tag == "MUSICBRAINZ_ALBUMID")
items.insert( std::make_pair(MetaData::Type::MusicBrainzAlbumID, stringTrim( values.front().to8Bit(true))));
else if (tag == "MUSICBRAINZ_TRACKID")
items.insert( std::make_pair(MetaData::Type::MusicBrainzRecordingID, stringTrim( values.front().to8Bit(true))));
else if (tag == "ACOUSTID_ID")
items.insert( std::make_pair(MetaData::Type::AcoustID, stringTrim( values.front().to8Bit(true))));
else if (tag == "TRACKTOTAL")
{
auto totalTrack = readAs<std::size_t>(values.front().to8Bit(true));
if (totalTrack)
items[MetaData::Type::TotalTrack] = *totalTrack;
}
else if (tag == "TRACKNUMBER")
{
// Expecting 'Number/Total'
auto strings = splitString(values.front().to8Bit(), "/");
if (!strings.empty())
{
auto number = readAs<std::size_t>(strings[0]);
if (number)
items.insert( std::make_pair(MetaData::Type::TrackNumber, *number ));
// Lower priority than TRACKTOTAL
if (strings.size() > 1 && items.find(MetaData::Type::TotalTrack) == items.end())
{
auto totalTrack = readAs<std::size_t>(strings[1]);
if (totalTrack)
items[MetaData::Type::TotalTrack] = *totalTrack;
}
}
}
else if (tag == "DISCTOTAL")
{
auto totalDisc = readAs<std::size_t>(values.front().to8Bit(true));
if (totalDisc)
items[MetaData::Type::TotalDisc] = *totalDisc;
}
else if (tag == "DISCNUMBER")
{
// Expecting 'Number/Total'
auto strings = splitString(values.front().to8Bit(), "/");
if (!strings.empty())
{
auto number = readAs<std::size_t>(strings[0]);
if (number)
items.insert( std::make_pair(MetaData::Type::DiscNumber, *number));
// Lower priority than DISCTOTAL
if (strings.size() > 1 && items.find(MetaData::Type::TotalDisc) == items.end())
{
auto totalDisc = readAs<std::size_t>(strings[1]);
if (totalDisc)
items[MetaData::Type::TotalDisc] = *totalDisc;
}
}
}
else if (tag == "DATE")
{
auto timePoint = readAs<int>(values.front().to8Bit());
if (timePoint)
items.insert( std::make_pair(MetaData::Type::Year, *timePoint));
}
else if (tag == "ORIGINALDATE")
{
// Lower priority than original year
if (items.find(MetaData::Type::OriginalYear) == items.end())
{
auto timePoint = readAs<int>(values.front().to8Bit());
if (timePoint)
items[MetaData::Type::OriginalYear] = *timePoint;
}
}
else if (tag == "ORIGINALYEAR")
{
auto timePoint = readAs<int>(values.front().to8Bit());
if (timePoint)
{
// Take priority on original year
items[MetaData::Type::OriginalYear] = *timePoint;
}
}
else if (tag == "METADATA_BLOCK_PICTURE")
{
// Only add once
if (items.find(MetaData::Type::HasCover) == items.end())
items.insert( std::make_pair(MetaData::Type::HasCover, true));
}
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
{
std::set<std::string> clusterNames;
for (const auto& value : values)
clusterNames.insert(value.to8Bit(true));
if (!clusterNames.empty())
clusters[tag] = clusterNames;
}
}
if (!clusters.empty())
items.insert( std::make_pair(MetaData::Type::Clusters, clusters) );
}
return items;
}
} // namespace MetaData
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2015 Emeric Poupon
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
@@ -19,19 +19,20 @@
#pragma once
#include <Wt/WContainerWidget>
#include <Wt/WWebWidget>
#include <map>
#include <string>
class InputRange : public Wt::WWebWidget
#include "MetaData.hpp"
namespace MetaData
{
// Parse that makes use of AvFormat
class TagLibParser : public Parser
{
public:
InputRange(Wt::WContainerWidget *parent = 0);
Wt::DomElementType domElementType() const
{
return Wt::DomElement_INPUT;
}
std::string jsRef() const;
boost::optional<Items> parse(const boost::filesystem::path& p, bool debug = false) override;
};
} // namespace MetaData
+742
View File
@@ -0,0 +1,742 @@
/*
* Copyright (C) 2013 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "MediaScanner.hpp"
#include <stdexcept>
#include <boost/filesystem.hpp>
#include <boost/asio/placeholders.hpp>
#include <Wt/WLocalDateTime.h>
#include "cover/CoverArtGrabber.hpp"
#include "database/Artist.hpp"
#include "database/Cluster.hpp"
#include "database/Release.hpp"
#include "database/ScanSettings.hpp"
#include "database/Track.hpp"
#include "utils/Logger.hpp"
#include "utils/Path.hpp"
#include "utils/Utils.hpp"
using namespace Database;
namespace {
Wt::WDate
getNextMonday(Wt::WDate current)
{
do
{
current = current.addDays(1);
} while (current.dayOfWeek() != 1);
return current;
}
Wt::WDate
getNextFirstOfMonth(Wt::WDate current)
{
do
{
current = current.addDays(1);
} while (current.day() != 1);
return current;
}
bool
isFileSupported(const boost::filesystem::path& file, const std::set<boost::filesystem::path>& extensions)
{
return (extensions.find(file.extension()) != extensions.end());
}
bool
isPathInParentPath(const boost::filesystem::path& path, const boost::filesystem::path& parentPath)
{
boost::filesystem::path curPath = path;
while (curPath.has_parent_path())
{
curPath = curPath.parent_path();
if (curPath == parentPath)
return true;
}
return false;
}
Artist::pointer
getArtist(Wt::Dbo::Session& session, const std::string& name, const std::string& mbid)
{
Artist::pointer artist;
// First try to get by MBID
if (!mbid.empty())
{
artist = Artist::getByMBID(session, mbid);
if (!artist)
artist = Artist::create(session, name, mbid);
return artist;
}
// Fall back on artist name (collisions may occur)
if (!name.empty())
{
for (Artist::pointer sameNamedArtist : Artist::getByName(session, name))
{
if (sameNamedArtist->getMBID().empty())
{
artist = sameNamedArtist;
break;
}
}
// No Artist found with the same name and without MBID -> creating
if (!artist)
artist = Artist::create(session, name);
return artist;
}
return Artist::pointer();
}
Release::pointer
getRelease(Wt::Dbo::Session& session, const std::string& name, const std::string& mbid)
{
Release::pointer release;
// First try to get by MBID
if (!mbid.empty())
{
release = Release::getByMBID(session, mbid );
if (!release)
release = Release::create(session, name, mbid);
return release;
}
// Fall back on release name (collisions may occur)
if (!name.empty())
{
for (Release::pointer sameNamedRelease : Release::getByName(session, name))
{
if (sameNamedRelease->getMBID().empty())
{
release = sameNamedRelease;
break;
}
}
// No release found with the same name and without MBID -> creating
if (!release)
release = Release::create(session, name);
return release;
}
return Release::pointer();
}
std::vector<Cluster::pointer>
getClusters(Wt::Dbo::Session& session, const MetaData::Clusters& clustersNames)
{
std::vector< Cluster::pointer > clusters;
for (auto clusterNames : clustersNames)
{
auto clusterType = ClusterType::getByName(session, clusterNames.first);
if (!clusterType)
continue;
for (auto clusterName : clusterNames.second)
{
auto cluster = clusterType->getCluster(clusterName);
if (!cluster)
cluster = Cluster::create(session, clusterType, clusterName);
clusters.push_back(cluster);
}
}
return clusters;
}
} // namespace
namespace Scanner {
MediaScanner::MediaScanner(Wt::Dbo::SqlConnectionPool& connectionPool)
: _running(false),
_scheduleTimer(_ioService),
_db(connectionPool)
{
_ioService.setThreadCount(1);
refreshScanSettings();
}
void
MediaScanner::restart(void)
{
stop();
start();
}
void
MediaScanner::start(void)
{
_running = true;
// post some jobs in the io_service
scheduleScan();
_ioService.start();
}
void
MediaScanner::stop(void)
{
_running = false;
_scheduleTimer.cancel();
_ioService.stop();
}
void
MediaScanner::scheduleImmediateScan()
{
_ioService.post([=]()
{
LMS_LOG(DBUPDATER, INFO) << "Schedule immediate scan";
scheduleScan(std::chrono::seconds(0));
});
}
void
MediaScanner::reschedule()
{
_ioService.post([=]()
{
LMS_LOG(DBUPDATER, INFO) << "Rescheduling scan";
scheduleScan();
});
}
void
MediaScanner::scheduleScan()
{
refreshScanSettings();
Wt::WDateTime now = Wt::WLocalDateTime::currentServerDateTime().toUTC();
Wt::WDate nextScanDate;
switch (_updatePeriod)
{
case ScanSettings::UpdatePeriod::Daily:
if (now.time() < _startTime)
nextScanDate = now.date();
else
nextScanDate = now.date().addDays(1);
break;
case ScanSettings::UpdatePeriod::Weekly:
if (now.time() < _startTime && now.date().dayOfWeek() == 1)
nextScanDate = now.date();
else
nextScanDate = getNextMonday(now.date());
break;
case ScanSettings::UpdatePeriod::Monthly:
if (now.time() < _startTime && now.date().day() == 1)
nextScanDate = now.date();
else
nextScanDate = getNextFirstOfMonth(now.date());
break;
case ScanSettings::UpdatePeriod::Never:
LMS_LOG(DBUPDATER, INFO) << "Auto scan disabled!";
break;
}
if (nextScanDate.isValid())
scheduleScan( Wt::WDateTime(nextScanDate, _startTime).toTimePoint() );
}
void
MediaScanner::scheduleScan(std::chrono::seconds duration)
{
LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan in " << duration.count() << " seconds";
_scheduleTimer.expires_from_now(std::chrono::seconds(5)); //duration);
_scheduleTimer.async_wait( std::bind( &MediaScanner::scan, this, std::placeholders::_1) );
}
void
MediaScanner::scheduleScan(std::chrono::system_clock::time_point timePoint)
{
std::time_t t = std::chrono::system_clock::to_time_t(timePoint);
LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan at " << std::string(std::ctime(&t));
_scheduleTimer.expires_at(timePoint);
_scheduleTimer.async_wait(std::bind(&MediaScanner::scan, this, std::placeholders::_1));
}
void
MediaScanner::scan(boost::system::error_code err)
{
if (err)
return;
LMS_LOG(UI, INFO) << "New scan started!";
refreshScanSettings();
bool forceScan = false;
Stats stats;
checkAudioFiles(stats);
LMS_LOG(UI, INFO) << "Checks complete, force scan = " << forceScan;
LMS_LOG(DBUPDATER, INFO) << "scaning media directory '" << _mediaDirectory.string() << "'...";
scanMediaDirectory(_mediaDirectory, forceScan, stats);
LMS_LOG(DBUPDATER, INFO) << "scaning media directory '" << _mediaDirectory.string() << "' DONE";
if (_running)
checkDuplicatedAudioFiles(stats);
LMS_LOG(DBUPDATER, INFO) << "Scan " << (_running ? "complete" : "aborted") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.scanErrors << ", not imported = " << stats.incompleteScans << "), duplicates = " << stats.nbDuplicates() << " (hash = " << stats.duplicateHashes << ", mbid = " << stats.duplicateMBID << ")";
// Save the last scan only if it has been completed
if (_running)
{
scheduleScan();
scanComplete().emit(stats);
}
}
void
MediaScanner::refreshScanSettings()
{
Wt::Dbo::Transaction transaction(_db.getSession());
auto scanSettings = ScanSettings::get(_db.getSession());
LMS_LOG(DBUPDATER, INFO) << "Using scan settings version " << scanSettings->getScanVersion();
_scanVersion = scanSettings->getScanVersion();
_startTime = scanSettings->getUpdateStartTime();
_updatePeriod = scanSettings->getUpdatePeriod();
_fileExtensions = scanSettings->getAudioFileExtensions();
_mediaDirectory = scanSettings->getMediaDirectory();
auto clusterTypes = scanSettings->getClusterTypes();
std::set<std::string> clusterTypeNames;
std::transform(clusterTypes.begin(), clusterTypes.end(),
std::inserter(clusterTypeNames, clusterTypeNames.begin()),
[](ClusterType::pointer clusterType) -> std::string { return clusterType->getName(); });
_metadataParser.setClusterTypeNames(clusterTypeNames);
}
void
MediaScanner::scanAudioFile(const boost::filesystem::path& file, bool forceScan, Stats& stats)
{
auto lastWriteTime = Wt::WDateTime::fromTime_t(boost::filesystem::last_write_time(file));
if (!forceScan)
{
// Skip file if last write is the same
Wt::Dbo::Transaction transaction(_db.getSession());
Wt::Dbo::ptr<Track> track = Track::getByPath(_db.getSession(), file);
if (track && track->getLastWriteTime() == lastWriteTime && track->getScanVersion() == _scanVersion)
{
stats.skips++;
return;
}
}
boost::optional<MetaData::Items> items = _metadataParser.parse(file);
if (!items)
{
stats.scanErrors++;
return;
}
stats.scans++;
std::vector<unsigned char> checksum ;
computeCrc(file, checksum);
Wt::Dbo::Transaction transaction(_db.getSession());
Wt::Dbo::ptr<Track> track = Track::getByPath(_db.getSession(), file);
// We estimate this is a audio file if:
// - we found a least one audio stream
// - the duration is not null
if ((*items).find(MetaData::Type::AudioStreams) == (*items).end()
|| boost::any_cast<std::vector<MetaData::AudioStream>> ((*items)[MetaData::Type::AudioStreams]).empty())
{
LMS_LOG(DBUPDATER, INFO) << "Skipped '" << file.string() << "' (no audio stream found)";
// If Track exists here, delete it!
if (track)
{
track.remove();
stats.deletions++;
}
stats.incompleteScans++;
return;
}
if ((*items).find(MetaData::Type::Duration) == (*items).end()
|| boost::any_cast<std::chrono::milliseconds>((*items)[MetaData::Type::Duration]).count() <= 0)
{
LMS_LOG(DBUPDATER, INFO) << "Skipped '" << file.string() << "' (no duration or duration <= 0)";
// If Track exists here, delete it!
if (track)
{
track.remove();
stats.deletions++;
}
stats.incompleteScans++;
return;
}
// ***** Title
std::string title;
if ((*items).find(MetaData::Type::Title) != (*items).end())
{
title = boost::any_cast<std::string>((*items)[MetaData::Type::Title]);
}
else
{
// TODO parse file name guess track etc.
// For now juste use file name as title
title = file.filename().string();
}
// ***** Clusters
std::vector< Cluster::pointer > genres;
{
MetaData::Clusters clusterNames;
if ((*items).find(MetaData::Type::Clusters) != (*items).end())
{
clusterNames = boost::any_cast<MetaData::Clusters> ((*items)[MetaData::Type::Clusters]);
}
genres = getClusters(_db.getSession(), clusterNames);
}
// ***** Artist
Artist::pointer artist;
{
std::string artistName;
std::string artistMusicBrainzID;
if ((*items).find(MetaData::Type::MusicBrainzArtistID) != (*items).end())
artistMusicBrainzID = boost::any_cast<std::string>((*items)[MetaData::Type::MusicBrainzArtistID] );
if ((*items).find(MetaData::Type::Artist) != (*items).end())
artistName = boost::any_cast<std::string>((*items)[MetaData::Type::Artist]);
artist = getArtist(_db.getSession(), artistName, artistMusicBrainzID);
}
// ***** Release
Release::pointer release;
{
std::string releaseName;
std::string releaseMusicBrainzID;
if ((*items).find(MetaData::Type::MusicBrainzAlbumID) != (*items).end())
releaseMusicBrainzID = boost::any_cast<std::string>((*items)[MetaData::Type::MusicBrainzAlbumID] );
if ((*items).find(MetaData::Type::Album) != (*items).end())
releaseName = boost::any_cast<std::string>((*items)[MetaData::Type::Album]);
release = getRelease(_db.getSession(), releaseName, releaseMusicBrainzID);
}
// If file already exist, update data
// Otherwise, create it
if (!track)
{
// Create a new song
track = Track::create(_db.getSession(), file);
LMS_LOG(DBUPDATER, INFO) << "Adding '" << file.string() << "'";
stats.additions++;
}
else
{
LMS_LOG(DBUPDATER, INFO) << "Updating '" << file.string() << "'";
// Remove the songs from its clusters
for (auto cluster : track->getClusters())
cluster.remove();
stats.updates++;
}
assert(track);
track.modify()->setScanVersion(_scanVersion);
track.modify()->setChecksum(checksum);
track.modify()->setArtist(artist);
track.modify()->setRelease(release);
track.modify()->setLastWriteTime(lastWriteTime);
track.modify()->setName(title);
track.modify()->setDuration( boost::any_cast<std::chrono::milliseconds>((*items)[MetaData::Type::Duration]) );
track.modify()->setAddedTime( Wt::WLocalDateTime::currentServerDateTime().toUTC() );
{
std::string trackClusterList;
// Product genre list
for (Cluster::pointer genre : genres)
{
if (!trackClusterList.empty())
trackClusterList += ", ";
trackClusterList += genre->getName();
genre.modify()->addTrack(track);
}
track.modify()->setGenres( trackClusterList );
}
if ((*items).find(MetaData::Type::TrackNumber) != (*items).end())
track.modify()->setTrackNumber( boost::any_cast<std::size_t>((*items)[MetaData::Type::TrackNumber]) );
if ((*items).find(MetaData::Type::TotalTrack) != (*items).end())
track.modify()->setTotalTrackNumber( boost::any_cast<std::size_t>((*items)[MetaData::Type::TotalTrack]) );
if ((*items).find(MetaData::Type::DiscNumber) != (*items).end())
track.modify()->setDiscNumber( boost::any_cast<std::size_t>((*items)[MetaData::Type::DiscNumber]) );
if ((*items).find(MetaData::Type::TotalDisc) != (*items).end())
track.modify()->setTotalDiscNumber( boost::any_cast<std::size_t>((*items)[MetaData::Type::TotalDisc]) );
if ((*items).find(MetaData::Type::Year) != (*items).end())
track.modify()->setYear( boost::any_cast<int>((*items)[MetaData::Type::Year]) );
if ((*items).find(MetaData::Type::OriginalYear) != (*items).end())
{
track.modify()->setOriginalYear( boost::any_cast<int>((*items)[MetaData::Type::OriginalYear]) );
// If a file has an OriginalYear but no Year, set it o ease filtering
if ((*items).find(MetaData::Type::Year) == (*items).end())
track.modify()->setYear( boost::any_cast<int>((*items)[MetaData::Type::OriginalYear]) );
}
if ((*items).find(MetaData::Type::MusicBrainzRecordingID) != (*items).end())
{
track.modify()->setMBID( boost::any_cast<std::string>((*items)[MetaData::Type::MusicBrainzRecordingID]) );
}
if ((*items).find(MetaData::Type::HasCover) != (*items).end())
{
bool hasCover = boost::any_cast<bool>((*items)[MetaData::Type::HasCover]);
track.modify()->setCoverType( hasCover ? Track::CoverType::Embedded : Track::CoverType::None );
}
transaction.commit();
}
void
MediaScanner::scanMediaDirectory(boost::filesystem::path mediaDirectory, bool forceScan, Stats& stats)
{
boost::system::error_code ec;
boost::filesystem::recursive_directory_iterator itPath(mediaDirectory, ec);
if (ec)
{
LMS_LOG(DBUPDATER, ERROR) << "Cannot iterate over '" << mediaDirectory.string() << "': " << ec.message();
return;
}
boost::filesystem::recursive_directory_iterator itEnd;
while (itPath != itEnd)
{
boost::filesystem::path path = *itPath;
if (!_running)
return;
if (ec)
{
LMS_LOG(DBUPDATER, ERROR) << "Cannot process entry: " << ec.message();
}
else if (boost::filesystem::is_regular(path))
{
if (isFileSupported(path, _fileExtensions))
scanAudioFile(path, forceScan, stats );
}
itPath.increment(ec);
}
}
// Check if a file exists and is still in a media directory
static bool
checkFile(const boost::filesystem::path& p, boost::filesystem::path mediaDirectory, const std::set<boost::filesystem::path>& extensions)
{
try
{
bool status = true;
// For each track, make sure the the file still exists
// and still belongs to a media directory
if (!boost::filesystem::exists( p )
|| !boost::filesystem::is_regular( p ) )
{
LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': missing";
status = false;
}
else if (!isPathInParentPath(p, mediaDirectory))
{
LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': out of media directory";
status = false;
}
else if (!isFileSupported(p, extensions))
{
LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': file format no longer handled";
status = false;
}
return status;
}
catch (boost::filesystem::filesystem_error& e)
{
LMS_LOG(DBUPDATER, ERROR) << "Caught exception while checking file '" << p.string() << "': " << e.what();
return false;
}
}
void
MediaScanner::checkAudioFiles( Stats& stats )
{
LMS_LOG(DBUPDATER, INFO) << "Checking audio files...";
std::vector<boost::filesystem::path> trackPaths = Track::getAllPaths(_db.getSession());;
LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks...";
for (auto& trackPath : trackPaths)
{
if (!_running)
return;
if (!checkFile(trackPath, _mediaDirectory, _fileExtensions))
{
Wt::Dbo::Transaction transaction(_db.getSession());
Track::pointer track = Track::getByPath(_db.getSession(), trackPath);
if (track)
{
track.remove();
stats.deletions++;
}
}
}
LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan clusters...";
{
Wt::Dbo::Transaction transaction(_db.getSession());
// TODO better query for this
// Now process orphan Cluster (no track)
auto clusters = Cluster::getAll(_db.getSession());
for (auto cluster : clusters)
{
if (cluster->getCount() == 0)
{
LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan cluster '" << cluster->getName() << "'";
cluster.remove();
}
}
}
LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan artists...";
{
Wt::Dbo::Transaction transaction(_db.getSession());
auto artists = Artist::getAllOrphans(_db.getSession());
for (auto artist : artists)
{
LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan artist '" << artist->getName() << "'";
artist.remove();
}
}
LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan releases...";
{
Wt::Dbo::Transaction transaction(_db.getSession());
auto releases = Release::getAllOrphans(_db.getSession());
for (auto release : releases)
{
LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan release '" << release->getName() << "'";
release.remove();
}
}
LMS_LOG(DBUPDATER, INFO) << "Check audio files done!";
}
void
MediaScanner::checkDuplicatedAudioFiles(Stats& stats)
{
LMS_LOG(DBUPDATER, INFO) << "Checking duplicated audio files";
Wt::Dbo::Transaction transaction(_db.getSession());
std::vector<Track::pointer> tracks = Database::Track::getMBIDDuplicates(_db.getSession());
for (Track::pointer track : tracks)
{
LMS_LOG(DBUPDATER, INFO) << "Found duplicated MBID [" << track->getMBID() << "], file: " << track->getPath().string() << " - " << track->getName();
stats.duplicateMBID++;
}
tracks = Database::Track::getChecksumDuplicates(_db.getSession());
for (Track::pointer track : tracks)
{
LMS_LOG(DBUPDATER, INFO) << "Found duplicated checksum [" << bufferToString(track->getChecksum()) << "], file: " << track->getPath().string() << " - " << track->getName();
stats.duplicateHashes++;
}
LMS_LOG(DBUPDATER, INFO) << "Checking duplicated audio files done!";
}
} // namespace Scanner
+111
View File
@@ -0,0 +1,111 @@
/*
* Copyright (C) 2013 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <chrono>
#include <Wt/WIOService.h>
#include <Wt/WSignal.h>
#include <boost/asio/system_timer.hpp>
#include "metadata/TagLibParser.hpp"
#include "database/ScanSettings.hpp"
#include "database/DatabaseHandler.hpp"
namespace Scanner {
class MediaScanner
{
public:
MediaScanner(Wt::Dbo::SqlConnectionPool& connectionPool);
void start();
void stop();
void restart();
void scheduleImmediateScan();
void reschedule();
struct Stats
{
std::size_t skips = 0; // no change since last scan
std::size_t scans = 0; // actually scanned filed
std::size_t scanErrors = 0; // cannot scan file
std::size_t incompleteScans = 0; // Scanned, but not imported (criteria not filled)
std::size_t additions = 0; // Added in DB
std::size_t deletions = 0; // removed from DB
std::size_t updates = 0; // updated file in DB
std::size_t duplicateHashes = 0; // Same file hashes
std::size_t duplicateMBID = 0; // Same MBID
std::size_t nbFiles() const { return skips + additions + updates; }
std::size_t nbChanges() const { return additions + deletions + updates; }
std::size_t nbErrors() const { return scanErrors + incompleteScans; }
std::size_t nbDuplicates() const { return duplicateHashes + duplicateMBID; }
};
// Called just after scan complete
Wt::Signal<Stats>& scanComplete() { return _sigScanComplete; }
private:
// Job handling
void scheduleScan();
void scheduleScan(std::chrono::seconds duration);
void scheduleScan(std::chrono::system_clock::time_point time);
// Update database (scheduled callback)
void scan(boost::system::error_code ec);
void scanMediaDirectory( boost::filesystem::path mediaDirectory, bool forceScan, Stats& stats);
// Helpers
void refreshScanSettings();
void checkAudioFiles( Stats& stats );
void checkDuplicatedAudioFiles( Stats& stats );
void scanAudioFile( const boost::filesystem::path& file, bool forceScan, Stats& stats);
bool _running;
Wt::WIOService _ioService;
Wt::Signal<Stats> _sigScanComplete;
boost::asio::system_timer _scheduleTimer;
Database::Handler _db;
// Current scan settings
std::size_t _scanVersion;
Wt::WTime _startTime;
Database::ScanSettings::UpdatePeriod _updatePeriod;
std::set<boost::filesystem::path> _fileExtensions;
boost::filesystem::path _mediaDirectory;
MetaData::TagLibParser _metadataParser;
}; // class MediaScanner
} // Scanner
-52
View File
@@ -1,52 +0,0 @@
/*
* Copyright (C) 2013 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <boost/thread.hpp>
#include "DatabaseUpdateService.hpp"
namespace Service {
DatabaseUpdateService::DatabaseUpdateService(Wt::Dbo::SqlConnectionPool &connectionPool)
: _metadataParser(),
_databaseUpdater( connectionPool, _metadataParser)
{
}
void
DatabaseUpdateService::start(void)
{
_databaseUpdater.start();
}
void
DatabaseUpdateService::stop(void)
{
_databaseUpdater.stop();
}
void
DatabaseUpdateService::restart(void)
{
stop();
start();
}
} // namespace Service
-55
View File
@@ -1,55 +0,0 @@
/*
* Copyright (C) 2013 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef DB_UPDATE_SERVICE_HPP
#define DB_UPDATE_SERVICE_HPP
#include <boost/thread.hpp>
#include <boost/asio/io_service.hpp>
#include "metadata/AvFormat.hpp"
#include "database-updater/DatabaseUpdater.hpp"
#include "Service.hpp"
namespace Service {
class DatabaseUpdateService : public Service
{
public:
typedef std::shared_ptr<DatabaseUpdateService> pointer;
DatabaseUpdateService(Wt::Dbo::SqlConnectionPool &connectionPool);
// Service interface
void start(void);
void stop(void);
void restart(void);
private:
MetaData::AvFormat _metadataParser;
DatabaseUpdater::Updater _databaseUpdater; // Todo use handler
};
} // namespace Service
#endif
-51
View File
@@ -1,51 +0,0 @@
/*
* Copyright (C) 2013 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef SERVICE_HPP
#define SERVICE_HPP
#include <boost/thread.hpp>
#include <memory>
#include <set>
namespace Service {
// Interface class wrapper for running services
class Service
{
public:
typedef std::shared_ptr<Service> pointer;
Service(const Service&) = delete;
Service& operator=(const Service&) = delete;
Service() {}
virtual ~Service() {}
virtual void start(void) = 0;
virtual void stop(void) = 0;
virtual void restart(void) = 0;
};
} // namespace Service
#endif
-84
View File
@@ -1,84 +0,0 @@
/*
* Copyright (C) 2013 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ServiceManager.hpp"
namespace Service {
ServiceManager&
ServiceManager::instance()
{
static ServiceManager instance;
return instance;
}
ServiceManager::ServiceManager()
{
}
ServiceManager::~ServiceManager()
{
stop();
}
void
ServiceManager::add(Service::pointer service)
{
_services.insert(service);
service->start();
}
void
ServiceManager::del(Service::pointer service)
{
service->stop();
_services.erase(service);
}
void
ServiceManager::clear(void)
{
stop();
_services.clear();
}
void
ServiceManager::start(void)
{
for (Service::pointer service : _services)
service->start();
}
void
ServiceManager::stop(void)
{
for (Service::pointer service : _services)
service->stop();
}
void
ServiceManager::restart(void)
{
for (Service::pointer service : _services)
service->restart();
}
} // namespace Service
-77
View File
@@ -1,77 +0,0 @@
/*
* Copyright (C) 2013 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef SERVICE_CONTROLER_HPP
#define SERVICE_CONTROLER_HPP
#include <set>
#include "Service.hpp"
namespace Service {
// Start/Stop/Reload Services
class ServiceManager
{
public:
static ServiceManager& instance();
~ServiceManager();
void add(Service::pointer service);
void del(Service::pointer service);
void clear();
void start();
void stop();
void restart();
template <class T> typename T::pointer get();
boost::mutex& mutex() { return _mutex;}
private:
ServiceManager();
ServiceManager(ServiceManager const&); // Don't Implement
void operator=(ServiceManager const&); // Don't implement
boost::mutex _mutex;
std::set<Service::pointer> _services;
};
template <class T> typename T::pointer
ServiceManager::get()
{
std::set<Service::pointer>::iterator it;
for (Service::pointer service : _services)
{
if (typeid(*(service)) == typeid(T)) {
return std::dynamic_pointer_cast<T>(service);
}
}
return std::shared_ptr<T>();
}
} // namespace Service
#endif
+101
View File
@@ -0,0 +1,101 @@
/*
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Auth.hpp"
#include <Wt/WFormModel.h>
#include <Wt/WLineEdit.h>
#include <Wt/WCheckBox.h>
#include <Wt/WPushButton.h>
#include "utils/Logger.hpp"
#include "common/Validators.hpp"
#include "LmsApplication.hpp"
namespace UserInterface {
Auth::Auth()
: Wt::WTemplateFormView(Wt::WString::tr("Lms.Auth.template"))
{
_model = std::make_shared<Wt::Auth::AuthModel>(LmsApp->getDb().getAuthService(), LmsApp->getDb().getUserDatabase());
_model->addPasswordAuth(&Database::Handler::getPasswordService());
// LoginName
setFormWidget(Wt::Auth::AuthModel::LoginNameField, std::make_unique<Wt::WLineEdit>());
{
Wt::Dbo::Transaction transaction(LmsApp->getDboSession());
auto demoUser = Database::User::getDemo(LmsApp->getDboSession());
if (demoUser)
{
Wt::Auth::User authUser = LmsApp->getDb().getUserDatabase().findWithId(std::to_string(demoUser.id()));
_model->setValue(Wt::Auth::AuthModel::LoginNameField, authUser.identity(Wt::Auth::Identity::LoginName));
_model->setValue(Wt::Auth::AuthModel::PasswordField, authUser.identity(Wt::Auth::Identity::LoginName));
}
}
// Password
auto password = std::make_unique<Wt::WLineEdit>();
password->setEchoMode(Wt::EchoMode::Password);
password->enterPressed().connect(this, &Auth::processAuth);
setFormWidget(Wt::Auth::AuthModel::PasswordField, std::move(password));
// Remember Me
setFormWidget(Wt::Auth::AuthModel::RememberMeField, std::make_unique<Wt::WCheckBox>());
Wt::WPushButton* loginBtn = bindNew<Wt::WPushButton>("login-btn", Wt::WString::tr("Lms.login"));
loginBtn->clicked().connect(this, &Auth::processAuth);
LmsApp->getDb().getLogin().changed().connect(std::bind([=]
{
if (LmsApp->getDb().getLogin().loggedIn())
this->setHidden(true);
}));
updateView(_model.get());
Wt::Auth::User user = _model->processAuthToken();
if (user.isValid())
{
LMS_LOG(UI, DEBUG) << "Valid user found from auth token (id = " << user.id() << ")";
_model->loginUser(LmsApp->getDb().getLogin(), user, Wt::Auth::LoginState::Weak);
}
}
void
Auth::processAuth()
{
updateModel(_model.get());
if (_model->validate())
_model->login(LmsApp->getDb().getLogin());
else
updateView(_model.get());
}
void
Auth::logout()
{
_model->logout(LmsApp->getDb().getLogin());
}
} // namespace UserInterface
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2016 Emeric Poupon
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
@@ -19,20 +19,25 @@
#pragma once
#include <Wt/WContainerWidget>
#include <Wt/WSignal>
#include <Wt/WTemplateFormView.h>
#include <Wt/Auth/AuthModel.h>
namespace UserInterface {
namespace Mobile {
class TrackSearchView : public Wt::WContainerWidget
class Auth : public Wt::WTemplateFormView
{
public:
Auth();
TrackSearchView(PlayQueueEvents& events, Wt::WContainerWidget* parent = 0);
void logout();
private:
void processAuth();
std::shared_ptr<Wt::Auth::AuthModel> _model;
};
} // namespace Mobile
} // namespace UserInterface
+480 -182
View File
@@ -17,60 +17,47 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <Wt/WEnvironment>
#include <Wt/WBootstrapTheme>
#include <Wt/WVBoxLayout>
#include <Wt/WNavigationBar>
#include <Wt/WStackedWidget>
#include <Wt/WMenu>
#include <Wt/WPopupMenu>
#include <Wt/WVBoxLayout>
#include <Wt/Auth/Identity>
#include "config/config.h"
#include "logger/Logger.hpp"
#include "utils/Utils.hpp"
#include "settings/Settings.hpp"
#include "settings/SettingsFirstConnectionFormView.hpp"
#include "auth/LmsAuth.hpp"
#include "audio/desktop/DesktopAudio.hpp"
#include "audio/mobile/MobileAudio.hpp"
#if HAVE_VIDEO
#include "video/VideoWidget.hpp"
#endif
#include "common/LineEdit.hpp"
#include "LmsApplication.hpp"
namespace skeletons {
extern const char *AuthStrings_xml1;
}
#include <Wt/WAnchor.h>
#include <Wt/WBootstrapTheme.h>
#include <Wt/WEnvironment.h>
#include <Wt/WMenu.h>
#include <Wt/WNavigationBar.h>
#include <Wt/WPopupMenu.h>
#include <Wt/WServer.h>
#include <Wt/WStackedWidget.h>
#include <Wt/WText.h>
#include <Wt/Auth/Identity.h>
namespace {
#include "config/config.h"
#include "utils/Logger.hpp"
#include "utils/Utils.hpp"
bool agentIsMobile()
{
const Wt::WEnvironment& env = Wt::WApplication::instance()->environment();
return (env.agentIsIEMobile()
|| env.agentIsMobileWebKit()
|| env.userAgent().find("Mobile") != std::string::npos // Workaround for firefox
|| env.userAgent().find("Tablet") != std::string::npos // Workaround for firefox
);
}
}
#include "explore/Explore.hpp"
#include "MediaPlayer.hpp"
#include "PlayHistoryView.hpp"
#include "PlayQueueView.hpp"
#include "SettingsView.hpp"
#include "admin/InitWizardView.hpp"
#include "admin/DatabaseSettingsView.hpp"
#include "admin/UserView.hpp"
#include "admin/UsersView.hpp"
#include "resource/ImageResource.hpp"
#include "resource/TranscodeResource.hpp"
namespace UserInterface {
Wt::WApplication*
LmsApplication::create(const Wt::WEnvironment& env, Wt::Dbo::SqlConnectionPool& connectionPool)
std::unique_ptr<Wt::WApplication>
LmsApplication::create(const Wt::WEnvironment& env, Wt::Dbo::SqlConnectionPool& connectionPool, LmsApplicationGroupContainer& appGroups, Scanner::MediaScanner& scanner)
{
/*
* You could read information from the environment to decide whether
* the user has permission to start a new application
*/
return new LmsApplication(env, connectionPool);
return std::make_unique<LmsApplication>(env, connectionPool, appGroups, scanner);
}
LmsApplication*
@@ -85,183 +72,494 @@ LmsApplication::instance()
* constructor so it is typically also an argument for your custom
* application constructor.
*/
LmsApplication::LmsApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnectionPool& connectionPool)
LmsApplication::LmsApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnectionPool& connectionPool, LmsApplicationGroupContainer& appGroups, Scanner::MediaScanner& scanner)
: Wt::WApplication(env),
_db(connectionPool),
_appGroups(appGroups),
_scanner(scanner),
_imageResource(nullptr),
_transcodeResource(nullptr)
{
Wt::WBootstrapTheme *bootstrapTheme = new Wt::WBootstrapTheme(this);
bootstrapTheme->setVersion(Wt::WBootstrapTheme::Version3);
auto bootstrapTheme = std::make_unique<Wt::WBootstrapTheme>();
bootstrapTheme->setVersion(Wt::BootstrapVersion::v3);
bootstrapTheme->setResponsive(true);
setTheme(bootstrapTheme);
setTheme(std::move(bootstrapTheme));
useStyleSheet("css/lms.css");
useStyleSheet("resources/font-awesome/css/font-awesome.min.css");
// Add a resource bundle
messageResourceBundle().use(appRoot() + "admin-database");
messageResourceBundle().use(appRoot() + "admin-user");
messageResourceBundle().use(appRoot() + "admin-users");
messageResourceBundle().use(appRoot() + "admin-initwizard");
messageResourceBundle().use(appRoot() + "artist");
messageResourceBundle().use(appRoot() + "artists");
messageResourceBundle().use(appRoot() + "artistsinfo");
messageResourceBundle().use(appRoot() + "explore");
messageResourceBundle().use(appRoot() + "login");
messageResourceBundle().use(appRoot() + "mediaplayer");
messageResourceBundle().use(appRoot() + "messages");
messageResourceBundle().use(appRoot() + "playqueue");
messageResourceBundle().use(appRoot() + "playhistory");
messageResourceBundle().use(appRoot() + "release");
messageResourceBundle().use(appRoot() + "releases");
messageResourceBundle().use(appRoot() + "releasesinfo");
messageResourceBundle().use(appRoot() + "settings");
messageResourceBundle().use(appRoot() + "templates");
messageResourceBundle().use(appRoot() + "tracks");
messageResourceBundle().use(appRoot() + "tracksinfo");
// Require js here to avoid async problems
requireJQuery("/js/jquery-1.10.2.min.js");
require("/js/mediaplayer.js");
require("/js/bootstrap-notify.js");
setTitle("LMS");
// If here is no account in the database, launch the first connection wizard
bool firstConnection;
{
Wt::Dbo::Transaction transaction(DboSession());
Wt::Dbo::Transaction transaction(LmsApp->getDboSession());
firstConnection = (Database::User::getAll(DboSession()).size() == 0);
firstConnection = (Database::User::getAll(LmsApp->getDboSession()).size() == 0);
}
LMS_LOG(UI, DEBUG) << "Creating root widget. First connection = " << std::boolalpha << firstConnection;
LMS_LOG(UI, DEBUG) << "Creating root widget. First connection = " << firstConnection;
// If here is no account in the database, launch the first connection wizard
if (firstConnection)
createFirstConnectionUI();
else
createLmsUI();
}
Database::Handler& DbHandler()
{
return LmsApplication::instance()->getDbHandler();
}
Wt::Dbo::Session& DboSession()
{
return DbHandler().getSession();
}
const Wt::Auth::User& CurrentAuthUser()
{
return DbHandler().getLogin().user();
}
Database::User::pointer CurrentUser()
{
return DbHandler().getCurrentUser();
}
ImageResource* SessionImageResource()
{
return LmsApplication::instance()->getImageResource();
}
TranscodeResource* SessionTranscodeResource()
{
return LmsApplication::instance()->getTranscodeResource();
}
void
LmsApplication::createFirstConnectionUI()
{
// Hack, use the auth widget builtin strings
builtinLocalizedStrings().useBuiltin(skeletons::AuthStrings_xml1);
root()->addWidget( new Settings::FirstConnectionFormView());
}
void
LmsApplication::createLmsUI()
{
_imageResource = new ImageResource(_db, root());
_transcodeResource = new TranscodeResource(_db, root());
DbHandler().getLogin().changed().connect(this, &LmsApplication::handleAuthEvent);
LmsAuth *authWidget = new LmsAuth();
authWidget->model()->addPasswordAuth(&Database::Handler::getPasswordService());
authWidget->setRegistrationEnabled(false);
authWidget->processEnvironment();
root()->addWidget(authWidget);
}
void
LmsApplication::handleAuthEvent(void)
{
if (!DbHandler().getLogin().loggedIn())
{
LMS_LOG(UI, INFO) << "User logged out, session = " << Wt::WApplication::instance()->sessionId();
root()->addWidget(std::make_unique<InitWizardView>());
}
else
{
LmsApp->getDb().getLogin().changed().connect(this, &LmsApplication::handleAuthEvent);
_auth = root()->addNew<Auth>();
}
}
quit("");
redirect("/");
void
LmsApplication::finalize()
{
LmsApplicationInfo info = LmsApplicationInfo::fromEnvironment(environment());
return;
getApplicationGroup().postOthers([info]
{
LmsApp->getGroupEvents().appClosed(info);
});
getApplicationGroup().leave();
preQuit().emit();
}
Wt::WLink
LmsApplication::createArtistLink(Database::Artist::pointer artist)
{
return Wt::WLink(Wt::LinkType::InternalPath, "/artist/" + std::to_string(artist.id()));
}
std::unique_ptr<Wt::WAnchor>
LmsApplication::createArtistAnchor(Database::Artist::pointer artist, bool addText)
{
auto res = std::make_unique<Wt::WAnchor>(createArtistLink(artist));
if (addText)
{
res->setTextFormat(Wt::TextFormat::Plain);
res->setText(Wt::WString::fromUTF8(artist->getName()));
}
LMS_LOG(UI, INFO) << "User '" << CurrentAuthUser().identity(Wt::Auth::Identity::LoginName) << "' logged in from '" << Wt::WApplication::instance()->environment().clientAddress() << "', user agent = " << Wt::WApplication::instance()->environment().agent() << ", session = " << Wt::WApplication::instance()->sessionId();
return res;
}
this->root()->setOverflow(Wt::WContainerWidget::OverflowHidden);
setConfirmCloseMessage("Closing LMS. Are you sure?");
Wt::WLink
LmsApplication::createReleaseLink(Database::Release::pointer release)
{
return Wt::WLink(Wt::LinkType::InternalPath, "/release/" + std::to_string(release.id()));
}
// Create a Vertical layout: top is the nav bar, bottom is the contents
Wt::WVBoxLayout *layout = new Wt::WVBoxLayout(this->root());
std::unique_ptr<Wt::WAnchor>
LmsApplication::createReleaseAnchor(Database::Release::pointer release, bool addText)
{
auto res = std::make_unique<Wt::WAnchor>(createReleaseLink(release));
Wt::WNavigationBar *navigation = new Wt::WNavigationBar();
navigation->setTitle("LMS", "https://github.com/epoupon/lms");
navigation->setResponsive(true);
navigation->addStyleClass("main-nav");
Wt::WStackedWidget *contentsStack = new Wt::WStackedWidget();
contentsStack->setOverflow(Wt::WContainerWidget::OverflowAuto);
contentsStack->addStyleClass("contents");
Wt::WMenu *menu = new Wt::WMenu(contentsStack);
navigation->addMenu(menu, Wt::AlignRight);
LineEdit *searchEdit = new LineEdit(500);
navigation->bindWidget("search", searchEdit);
searchEdit->setEmptyText("Search...");
searchEdit->addStyleClass("navbar-form navbar-nav");
searchEdit->setWidth(150);
// TODO add a span with a search icon
menu->setInternalPathEnabled();
menu->setInternalBasePath("/");
Audio *audio;
if (agentIsMobile())
audio = new Mobile::Audio();
else
audio = new Desktop::Audio();
menu->addItem("Audio", audio)->setPathComponent("audio");;
#if defined HAVE_VIDEO
menu->addItem("Video", new VideoWidget())->setPathComponent("video");
#endif
menu->addItem("Settings", new Settings::Settings())->setPathComponent("settings");
Wt::WPopupMenu *popup = new Wt::WPopupMenu();
popup->addItem("Logout");
Wt::WMenuItem *item = new Wt::WMenuItem( CurrentAuthUser().identity(Wt::Auth::Identity::LoginName) );
item->setMenu(popup);
menu->addItem(item);
popup->itemSelected().connect(std::bind([=] (Wt::WMenuItem* item)
if (addText)
{
if (item && item->text() == "Logout")
DbHandler().getLogin().logout();
}, std::placeholders::_1));
res->setTextFormat(Wt::TextFormat::Plain);
res->setText(Wt::WString::fromUTF8(release->getName()));
}
searchEdit->timedChanged().connect(std::bind([=] ()
return res;
}
std::unique_ptr<Wt::WTemplate>
LmsApplication::createCluster(Database::Cluster::pointer cluster, bool canDelete)
{
std::string styleClass = "Lms-cluster-type-" + std::to_string(cluster->getType().id() % 10);
auto res = std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.template.cluster-entry").arg(styleClass));
res->addFunction("tr", &Wt::WTemplate::Functions::tr);
res->bindString("name", Wt::WString::fromUTF8(cluster->getName()), Wt::TextFormat::Plain);
res->setCondition("if-can-delete", canDelete);
res->setStyleClass("Lms-cluster");
res->setToolTip(cluster->getType()->getName(), Wt::TextFormat::Plain);
res->setInline(true);
return res;
}
void
LmsApplication::goHome()
{
setInternalPath("/artists", true);
}
void
LmsApplication::goHomeAndQuit()
{
setConfirmCloseMessage("");
WApplication::quit("");
redirect("/");
}
enum IdxRoot
{
IdxExplore = 0,
IdxPlayQueue,
IdxPlayHistory,
IdxSettings,
IdxAdminDatabase,
IdxAdminUsers,
IdxAdminUser,
};
static void
handlePathChange(Wt::WStackedWidget* stack, bool isAdmin)
{
static const struct
{
// TODO: check which view is activated and search into it
audio->search(searchEdit->text().toUTF8());
std::string path;
int index;
bool admin;
} views[] =
{
{ "/artists", IdxExplore, false },
{ "/artist", IdxExplore, false },
{ "/releases", IdxExplore, false },
{ "/release", IdxExplore, false },
{ "/tracks", IdxExplore, false },
{ "/playqueue", IdxPlayQueue, false },
{ "/playhistory", IdxPlayHistory, false },
{ "/settings", IdxSettings, false },
{ "/admin/database", IdxAdminDatabase, true },
{ "/admin/users", IdxAdminUsers, true },
{ "/admin/user", IdxAdminUser, true },
};
LMS_LOG(UI, DEBUG) << "Internal path changed to '" << wApp->internalPath() << "'";
for (auto& view : views)
{
if (wApp->internalPathMatches(view.path))
{
if (view.admin && !isAdmin)
break;
stack->setCurrentIndex(view.index);
return;
}
}
wApp->setInternalPath("/artists", true);
}
LmsApplicationGroup&
LmsApplication::getApplicationGroup()
{
return _appGroups.get(_userIdentity);
}
void
LmsApplication::handleAuthEvent()
{
try
{
if (!getDb().getLogin().loggedIn())
{
LMS_LOG(UI, INFO) << "User '" << _userIdentity << " 'logged out, session = " << sessionId();
goHomeAndQuit();
return;
}
else
{
_userIdentity = getAuthUser().identity(Wt::Auth::Identity::LoginName);
LmsApplicationInfo info = LmsApplicationInfo::fromEnvironment(environment());
LMS_LOG(UI, INFO) << "User '" << _userIdentity << "' logged in from '" << environment().clientAddress() << "', user agent = " << environment().userAgent() << ", session = " << sessionId();
getApplicationGroup().join(info);
getApplicationGroup().postOthers([info]
{
LmsApp->getGroupEvents().appOpen(info);
});
createHome();
}
}
catch (std::exception& e)
{
LMS_LOG(UI, ERROR) << "Error while handling auth event: " << e.what();
throw LmsException("Internal error"); // Do not put details here at it appears on the user rendered html
}
}
void
LmsApplication::createHome()
{
// Handle Media Scanner events and other session events
enableUpdates(true);
{
Wt::Dbo::Transaction transaction (LmsApp->getDboSession());
_isAdmin = LmsApp->getUser()->isAdmin();
}
_imageResource = std::make_shared<ImageResource>(_db);
_transcodeResource = std::make_shared<TranscodeResource>(_db);
setConfirmCloseMessage(Wt::WString::tr("Lms.quit-confirm"));
Wt::WTemplate* main = root()->addWidget(std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.template")));
// Navbar
Wt::WNavigationBar* navbar = main->bindNew<Wt::WNavigationBar>("navbar-top");
navbar->setTitle("LMS", Wt::WLink(Wt::LinkType::InternalPath, "/artists"));
navbar->setResponsive(true);
Wt::WMenu* menu = navbar->addMenu(std::make_unique<Wt::WMenu>());
{
auto menuItem = menu->insertItem(0, Wt::WString::tr("Lms.Explore.artists"));
menuItem->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/artists"));
menuItem->setSelectable(false);
}
{
auto menuItem = menu->insertItem(1, Wt::WString::tr("Lms.Explore.releases"));
menuItem->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/releases"));
menuItem->setSelectable(false);
}
{
auto menuItem = menu->insertItem(2, Wt::WString::tr("Lms.Explore.tracks"));
menuItem->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/tracks"));
menuItem->setSelectable(false);
}
{
auto menuItem = menu->insertItem(3, Wt::WString::tr("Lms.PlayQueue.playqueue"));
menuItem->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/playqueue"));
menuItem->setSelectable(false);
}
{
auto menuItem = menu->insertItem(4, Wt::WString::tr("Lms.PlayHistory.playhistory"));
menuItem->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/playhistory"));
menuItem->setSelectable(false);
}
Wt::WMenu* rightMenu = navbar->addMenu(std::make_unique<Wt::WMenu>(), Wt::AlignmentFlag::Right);
std::size_t itemCounter = 0;
if (_isAdmin)
{
auto menuItem = rightMenu->insertItem(itemCounter++, Wt::WString::tr("Lms.administration"));
menuItem->setSelectable(false);
auto admin = std::make_unique<Wt::WPopupMenu>();
auto dbSettings = admin->insertItem(0, Wt::WString::tr("Lms.Admin.Database.database"));
dbSettings->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/admin/database"));
dbSettings->setSelectable(false);
auto usersSettings = admin->insertItem(1, Wt::WString::tr("Lms.Admin.Users.users"));
usersSettings->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/admin/users"));
usersSettings->setSelectable(false);
menuItem->setMenu(std::move(admin));
}
{
auto menuItem = rightMenu->insertItem(itemCounter++, Wt::WString::tr("Lms.Settings.settings"));
menuItem->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/settings"));
menuItem->setSelectable(false);
}
{
auto menuItem = rightMenu->insertItem(itemCounter++, Wt::WString::tr("Lms.logout"));
menuItem->setSelectable(true);
menuItem->triggered().connect(std::bind([=]
{
setConfirmCloseMessage("");
_auth->logout();
}));
}
// Contents
// Order is important in mainStack, see IdxRoot!
Wt::WStackedWidget* mainStack = main->bindNew<Wt::WStackedWidget>("contents");
Explore* explore = mainStack->addNew<Explore>();
PlayQueue* playqueue = mainStack->addNew<PlayQueue>();
PlayHistory* playhistory = mainStack->addNew<PlayHistory>();
mainStack->addNew<SettingsView>();
// Admin stuff
if (_isAdmin)
{
mainStack->addNew<DatabaseSettingsView>();
mainStack->addNew<UsersView>();
mainStack->addNew<UserView>();
}
explore->tracksAdd.connect([=] (std::vector<Database::Track::pointer> tracks)
{
playqueue->addTracks(tracks);
});
explore->tracksPlay.connect([=] (std::vector<Database::Track::pointer> tracks)
{
playqueue->playTracks(tracks);
});
// MediaPlayer
MediaPlayer* player = main->bindNew<MediaPlayer>("player");
// Events from MediaPlayer
player->playNext.connect([=]
{
playqueue->playNext();
});
player->playPrevious.connect([=]
{
playqueue->playPrevious();
});
player->playbackEnded.connect([=]
{
playqueue->playNext();
});
// Events from the PlayQueue
playqueue->playTrack.connect(playhistory, &PlayHistory::addTrack);
playqueue->playTrack.connect(explore, &Explore::handleTrackPlayed);
playqueue->playTrack.connect(player, &MediaPlayer::playTrack);
playqueue->playbackStop.connect(player, &MediaPlayer::stop);
// Events from MediaScanner
std::string sessionId = LmsApp->sessionId();
_scanner.scanComplete().connect([=] (Scanner::MediaScanner::Stats stats)
{
Wt::WServer::instance()->post(sessionId, [=]
{
bool changes = false;
if (_isAdmin)
{
notifyMsg(MsgType::Info, Wt::WString::tr("Lms.Admin.Database.scan-complete")
.arg(static_cast<unsigned>(stats.nbFiles()))
.arg(static_cast<unsigned>(stats.additions))
.arg(static_cast<unsigned>(stats.updates))
.arg(static_cast<unsigned>(stats.deletions))
.arg(static_cast<unsigned>(stats.nbDuplicates()))
.arg(static_cast<unsigned>(stats.nbErrors())));
changes = true;
}
if (stats.nbChanges() > 0)
{
explore->handleDbChanged();
changes = true;
}
if (changes)
triggerUpdate();
});
});
// Events from Application group
_groupEvents.appOpen.connect([=] (LmsApplicationInfo info)
{
// Only one active session by user
if (!LmsApp->getUser()->isDemo())
{
setConfirmCloseMessage("");
quit(Wt::WString::tr("Lms.quit-other-session"));
}
});
_groupEvents.appClosed.connect([=] (LmsApplicationInfo info)
{
;
});
internalPathChanged().connect(std::bind([=]
{
handlePathChange(mainStack, _isAdmin);
}));
layout->addWidget(navigation);
layout->addWidget(contentsStack, 1);
layout->setContentsMargins(0, 0, 0, 0);
if (agentIsMobile())
setInternalPath("/audio/search/preview", true);
else
setInternalPath("/audio");
handlePathChange(mainStack, _isAdmin);
}
void
LmsApplication::notify(const Wt::WEvent& event)
{
try
{
WApplication::notify(event);
}
catch (std::exception& e)
{
LMS_LOG(UI, ERROR) << "Caught exception: " << e.what();
throw LmsException("Internal error"); // Do not put details here at it appears on the user rendered html
}
}
static std::string msgTypeToString(MsgType type)
{
switch(type)
{
case MsgType::Success: return "success";
case MsgType::Info: return "info";
case MsgType::Warning: return "warning";
case MsgType::Danger: return "danger";
}
return "";
}
void
LmsApplication::post(std::function<void()> func)
{
Wt::WServer::instance()->post(LmsApp->sessionId(), func);
}
void
LmsApplication::notifyMsg(MsgType type, const Wt::WString& message, std::chrono::milliseconds duration)
{
LMS_LOG(UI, INFO) << "Notifying message '" << message.toUTF8() << "' of type '" << msgTypeToString(type);
std::ostringstream oss;
oss << "$.notify({"
"message: '" << message.toUTF8() << "'"
"},{"
"type: '" << msgTypeToString(type) << "',"
"placement: {from: 'top', align: 'center'},"
"timer: 250,"
"delay: " << duration.count() << ""
"});";
LmsApp->doJavaScript(oss.str());
}
} // namespace UserInterface
+78 -27
View File
@@ -20,53 +20,104 @@
#ifndef LMS_APPLICATION_HPP
#define LMS_APPLICATION_HPP
#include <boost/filesystem.hpp>
#include <Wt/WApplication>
#include <Wt/Dbo/SqlConnectionPool>
#include <Wt/WApplication.h>
#include <Wt/Dbo/SqlConnectionPool.h>
#include "database/DatabaseHandler.hpp"
#include "resource/ImageResource.hpp"
#include "resource/TranscodeResource.hpp"
#include "scanner/MediaScanner.hpp"
#include "database/Artist.hpp"
#include "database/Cluster.hpp"
#include "database/Release.hpp"
#include "LmsApplicationGroup.hpp"
#include "Auth.hpp"
namespace UserInterface {
class TranscodeResource;
class ImageResource;
struct GroupEvents
{
Wt::Signal<LmsApplicationInfo> appOpen;
Wt::Signal<LmsApplicationInfo> appClosed;
};
enum class MsgType
{
Success,
Info,
Warning,
Danger,
};
class LmsApplication : public Wt::WApplication
{
public:
static Wt::WApplication *create(const Wt::WEnvironment& env, Wt::Dbo::SqlConnectionPool& connectionPool);
LmsApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnectionPool& connectionPool, LmsApplicationGroupContainer& appGroups, Scanner::MediaScanner& scanner);
static std::unique_ptr<Wt::WApplication> create(const Wt::WEnvironment& env,
Wt::Dbo::SqlConnectionPool& connectionPool, LmsApplicationGroupContainer& appGroups, Scanner::MediaScanner& scanner);
static LmsApplication* instance();
LmsApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnectionPool& connectionPool);
// Session application data
ImageResource* getImageResource() { return _imageResource; }
TranscodeResource* getTranscodeResource() { return _transcodeResource; }
Database::Handler& getDbHandler() { return _db;}
std::shared_ptr<ImageResource> getImageResource() { return _imageResource; }
std::shared_ptr<TranscodeResource> getTranscodeResource() { return _transcodeResource; }
Database::Handler& getDb() { return _db;}
Wt::Dbo::Session& getDboSession() { return _db.getSession();}
const Wt::Auth::User& getAuthUser() { return _db.getLogin().user(); }
Database::User::pointer getUser() { return _db.getCurrentUser(); }
Wt::WString getUserIdentity() { return _userIdentity; }
Scanner::MediaScanner& getMediaScanner() { return _scanner; }
GroupEvents& getGroupEvents() { return _groupEvents; }
// Utils
void goHome();
void goHomeAndQuit();
void post(std::function<void()> func);
void notifyMsg(MsgType type, const Wt::WString& message, std::chrono::milliseconds duration = std::chrono::milliseconds(4000));
static Wt::WLink createArtistLink(Database::Artist::pointer artist);
static std::unique_ptr<Wt::WAnchor> createArtistAnchor(Database::Artist::pointer artist, bool addText = true);
static Wt::WLink createReleaseLink(Database::Release::pointer release);
static std::unique_ptr<Wt::WAnchor> createReleaseAnchor(Database::Release::pointer release, bool addText = true);
static std::unique_ptr<Wt::WTemplate> createCluster(Database::Cluster::pointer cluster, bool canDelete = false);
// Signal emitted just before the session ends (user may already be logged out)
Wt::Signal<>& preQuit() { return _preQuit; }
private:
void handleAuthEvent(void);
void createFirstConnectionUI();
void createLmsUI();
LmsApplicationGroup& getApplicationGroup();
// Events
void handleAuthEvent();
void notify(const Wt::WEvent& event) override;
void finalize() override;
void createHome();
Wt::Signal<> _preQuit;
Database::Handler _db;
ImageResource* _imageResource;
TranscodeResource* _transcodeResource;
LmsApplicationGroupContainer& _appGroups;
GroupEvents _groupEvents;
Wt::WString _userIdentity;
Auth* _auth;
Scanner::MediaScanner& _scanner;
std::shared_ptr<ImageResource> _imageResource;
std::shared_ptr<TranscodeResource> _transcodeResource;
bool _isAdmin = false;
};
// Helpers to get session data
// Helper to get session instance
#define LmsApp LmsApplication::instance()
Database::Handler& DbHandler();
Wt::Dbo::Session& DboSession();
const Wt::Auth::User& CurrentAuthUser();
Database::User::pointer CurrentUser();
ImageResource *SessionImageResource();
TranscodeResource *SessionTranscodeResource();
} // namespace UserInterface
#endif
+89
View File
@@ -0,0 +1,89 @@
/*
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "LmsApplicationGroup.hpp"
#include <algorithm>
#include <Wt/WServer.h>
#include <Wt/WApplication.h>
namespace UserInterface {
LmsApplicationInfo
LmsApplicationInfo::fromEnvironment(const Wt::WEnvironment& env)
{
LmsApplicationInfo info = {.userAgent = wApp->environment().agent()};
return info;
}
void
LmsApplicationGroup::join(LmsApplicationInfo info)
{
std::lock_guard<std::mutex> lock(_mutex);
_apps.emplace(wApp->sessionId(), std::move(info));
}
void
LmsApplicationGroup::leave()
{
std::lock_guard<std::mutex> lock(_mutex);
_apps.erase(wApp->sessionId());
}
std::vector<std::string>
LmsApplicationGroup::getOtherSessionIds() const
{
std::vector<std::string> res;
std::lock_guard<std::mutex> lock(_mutex);
for (auto const& app : _apps)
{
if (app.first != wApp->sessionId())
res.push_back(app.first);
}
return res;
}
void
LmsApplicationGroup::postOthers(std::function<void()> func) const
{
for (auto const& sessionId : getOtherSessionIds())
{
Wt::WServer::instance()->post(sessionId, [=]
{
func();
wApp->triggerUpdate();
});
}
}
LmsApplicationGroup&
LmsApplicationGroupContainer::get(Wt::WString identity)
{
std::lock_guard<std::mutex> lock(_mutex);
return _apps[identity];
}
} // UserInterface
+66
View File
@@ -0,0 +1,66 @@
/*
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <map>
#include <mutex>
#include <Wt/WSignal.h>
#include <Wt/WEnvironment.h>
namespace UserInterface {
struct LmsApplicationInfo
{
Wt::UserAgent userAgent;
static LmsApplicationInfo fromEnvironment(const Wt::WEnvironment& env);
};
// LmsApplication instances are grouped by User
class LmsApplicationGroup
{
public:
void join(LmsApplicationInfo info);
void leave();
void postOthers(std::function<void()> func) const;
private:
mutable std::mutex _mutex;
std::vector<std::string> getOtherSessionIds() const;
std::map<std::string, LmsApplicationInfo> _apps;
};
class LmsApplicationGroupContainer
{
public:
LmsApplicationGroup& get(Wt::WString identity);
private:
std::map<Wt::WString, LmsApplicationGroup> _apps;
std::mutex _mutex;
};
} // UserInterface
+118
View File
@@ -0,0 +1,118 @@
/*
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "MediaPlayer.hpp"
#include "av/AvInfo.hpp"
#include "utils/Logger.hpp"
#include "database/Track.hpp"
#include "resource/ImageResource.hpp"
#include "resource/TranscodeResource.hpp"
#include "LmsApplication.hpp"
namespace UserInterface {
MediaPlayer::MediaPlayer()
: Wt::WTemplate(Wt::WString::tr("Lms.MediaPlayer.template")),
playbackEnded(this, "playbackEnded"),
playPrevious(this, "playPrevious"),
playNext(this, "playNext")
{
_title = bindNew<Wt::WText>("title");
_title->setTextFormat(Wt::TextFormat::Plain);
_artist = bindNew<Wt::WAnchor>("artist");
_artist->setTextFormat(Wt::TextFormat::Plain);
_release = bindNew<Wt::WAnchor>("release");
_release->setTextFormat(Wt::TextFormat::Plain);
wApp->doJavaScript("LMS.mediaplayer.init(" + jsRef() + ")");
}
void
MediaPlayer::playTrack(Database::IdType trackId)
{
LMS_LOG(UI, DEBUG) << "Playing track ID = " << trackId;
Wt::Dbo::Transaction transaction(LmsApp->getDboSession());
auto track = Database::Track::getById(LmsApp->getDboSession(), trackId);
try
{
Av::MediaFile mediaFile(track->getPath());
auto resource = LmsApp->getTranscodeResource()->getUrl(trackId, Av::Encoding::MP3);
auto imgResource = LmsApp->getImageResource()->getTrackUrl(trackId, 64);
std::ostringstream oss;
oss
<< "var params = {"
<< " resource: \"" << resource << "\","
<< " duration: " << std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count() << ","
<< " imgResource: \"" << imgResource << "\","
<< "};";
oss << "LMS.mediaplayer.loadTrack(params, true)"; // true to autoplay
LMS_LOG(UI, DEBUG) << "Runing js = '" << oss.str() << "'";
_title->setText(Wt::WString::fromUTF8(track->getName()));
if (track->getArtist())
{
_artist->setText(Wt::WString::fromUTF8(track->getArtist()->getName()));
_artist->setLink(LmsApp->createArtistLink(track->getArtist()));
}
else
{
_artist->setText("");
_artist->setLink(Wt::WLink());
}
if (track->getRelease())
{
_release->setText(Wt::WString::fromUTF8(track->getRelease()->getName()));
_release->setLink(LmsApp->createReleaseLink(track->getRelease()));
}
else
{
_release->setText("");
_release->setLink(Wt::WLink());
}
wApp->doJavaScript(oss.str());
}
catch (Av::MediaFileException& e)
{
LMS_LOG(UI, ERROR) << "MediaFileException: " << e.what();
}
}
void
MediaPlayer::stop()
{
wApp->doJavaScript("LMS.mediaplayer.stop()");
}
} // namespace UserInterface
+52
View File
@@ -0,0 +1,52 @@
/*
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <Wt/WAnchor.h>
#include <Wt/WJavaScript.h>
#include <Wt/WTemplate.h>
#include <Wt/WText.h>
#include "database/Types.hpp"
namespace UserInterface {
class MediaPlayer : public Wt::WTemplate
{
public:
MediaPlayer();
void stop();
void playTrack(Database::IdType trackId);
// Signals
Wt::JSignal<> playbackEnded;
Wt::JSignal<> playPrevious;
Wt::JSignal<> playNext;
private:
Wt::WText* _title;
Wt::WAnchor* _release;
Wt::WAnchor* _artist;
};
} // namespace UserInterface
+104
View File
@@ -0,0 +1,104 @@
/*
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "PlayHistoryView.hpp"
#include <Wt/WAnchor.h>
#include <Wt/WText.h>
#include "utils/Logger.hpp"
#include "database/Track.hpp"
#include "database/TrackList.hpp"
#include "LmsApplication.hpp"
namespace {
using namespace UserInterface;
std::unique_ptr<Wt::WTemplate> createEntry(Database::Track::pointer track)
{
auto entry = std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.PlayHistory.template.entry"));
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
auto artist = track->getArtist();
if (artist)
{
entry->setCondition("if-has-artist", true);
entry->bindWidget("artist-name", LmsApplication::createArtistAnchor(track->getArtist()));
}
auto release = track->getRelease();
if (release)
{
entry->setCondition("if-has-release", true);
entry->bindWidget("release-name", LmsApplication::createReleaseAnchor(track->getRelease()));
}
return entry;
}
}
namespace UserInterface {
PlayHistory::PlayHistory()
: Wt::WTemplate(Wt::WString::tr("Lms.PlayHistory.template"))
{
addFunction("tr", &Wt::WTemplate::Functions::tr);
_entriesContainer = bindNew<Wt::WContainerWidget>("entries");
_showMore = bindNew<Wt::WPushButton>("show-more", Wt::WString::tr("Lms.Explore.show-more"));
_showMore->setHidden(true);
_showMore->clicked().connect([=]
{
addSome();
});
addSome();
}
void
PlayHistory::addTrack(Database::IdType trackId)
{
Wt::Dbo::Transaction transaction (LmsApp->getDboSession());
auto trackEntry = LmsApp->getUser()->getPlayedTrackList().modify()->add(trackId);
_entriesContainer->insertWidget(0, createEntry(trackEntry->getTrack()));
}
void
PlayHistory::addSome()
{
Wt::Dbo::Transaction transaction (LmsApp->getDboSession());
auto trackList = LmsApp->getUser()->getPlayedTrackList();
auto trackEntries = trackList->getEntriesReverse(_entriesContainer->count(), 50);
for (auto trackEntry : trackEntries)
_entriesContainer->addWidget(createEntry(trackEntry->getTrack()));
_showMore->setHidden(static_cast<std::size_t>(_entriesContainer->count()) >= trackList->getCount());
}
} // namespace UserInterface
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2015 Emeric Poupon
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
@@ -19,29 +19,28 @@
#pragma once
#include <Wt/WContainerWidget>
#include "audio/Audio.hpp"
#include "MobilePlayQueueEvents.hpp"
#include <Wt/WContainerWidget.h>
#include <Wt/WPushButton.h>
#include <Wt/WTemplate.h>
#include "database/Types.hpp"
namespace UserInterface {
namespace Mobile {
class Audio : public UserInterface::Audio
class PlayHistory : public Wt::WTemplate
{
public:
Audio(Wt::WContainerWidget *parent = 0);
PlayHistory();
void search(std::string text);
void addTrack(Database::IdType trackId);
private:
PlayQueueEvents _playQueueEvents;
void addSome();
Wt::WContainerWidget* _entriesContainer;
Wt::WPushButton* _showMore;
};
} // namespace Mobile
} // namespace UserInterface
+376
View File
@@ -0,0 +1,376 @@
/*
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "PlayQueueView.hpp"
#include <random>
#include <Wt/WAnchor.h>
#include <Wt/WText.h>
#include "utils/Logger.hpp"
#include "database/TrackList.hpp"
#include "LmsApplication.hpp"
namespace UserInterface {
PlayQueue::PlayQueue()
: Wt::WTemplate(Wt::WString::tr("Lms.PlayQueue.template"))
{
addFunction("tr", &Wt::WTemplate::Functions::tr);
Wt::WText* clearBtn = bindNew<Wt::WText>("clear-btn", Wt::WString::tr("Lms.PlayQueue.clear"), Wt::TextFormat::XHTML);
_entriesContainer = bindNew<Wt::WContainerWidget>("entries");
_showMore = bindNew<Wt::WPushButton>("show-more", Wt::WString::tr("Lms.Explore.show-more"));
_showMore->setHidden(true);
Wt::WText* shuffleBtn = bindNew<Wt::WText>("shuffle-btn", Wt::WString::tr("Lms.PlayQueue.shuffle"), Wt::TextFormat::XHTML);
_radioMode = bindNew<Wt::WCheckBox>("radio-mode", Wt::WString::tr("Lms.PlayQueue.radio-mode"));
_nbTracks = bindNew<Wt::WText>("nb-tracks");
clearBtn->clicked().connect([=]
{
clearTracks();
});
shuffleBtn->clicked().connect([=]
{
{
Wt::Dbo::Transaction transaction(LmsApp->getDboSession());
getTrackList().modify()->shuffle();
}
_entriesContainer->clear();
addSome();
});
_showMore->clicked().connect([=]
{
addSome();
updateCurrentTrack(true);
});
LmsApp->preQuit().connect([=]
{
if (_tracklistId)
{
LMS_LOG(UI, DEBUG) << "Removing tracklist id " << *_tracklistId;
Wt::Dbo::Transaction transaction(LmsApp->getDboSession());
auto tracklist = Database::TrackList::getById(LmsApp->getDboSession(), *_tracklistId);
if (tracklist)
tracklist.remove();
}
});
updateInfo();
addSome();
if (!LmsApp->getUser()->isDemo())
{
LmsApp->post([=]
{
play(LmsApp->getUser()->getCurPlayingTrackPos());
});
}
}
Database::TrackList::pointer
PlayQueue::getTrackList()
{
static const std::string currentPlayQueueName = "__current__playqueue__";
Database::TrackList::pointer res;
if (LmsApp->getUser()->isDemo())
{
if (!_tracklistId)
{
res = Database::TrackList::create(LmsApp->getDboSession(), currentPlayQueueName, false, LmsApp->getUser());
LmsApp->getDboSession().flush();
_tracklistId = res.id();
return res;
}
return Database::TrackList::getById(LmsApp->getDboSession(), *_tracklistId);
}
return LmsApp->getUser()->getQueuedTrackList();
}
void
PlayQueue::clearTracks()
{
Wt::Dbo::Transaction transaction(LmsApp->getDboSession());
getTrackList().modify()->clear();
_showMore->setHidden(true);
_entriesContainer->clear();
updateInfo();
}
void
PlayQueue::stop()
{
updateCurrentTrack(false);
_trackPos.reset();
playbackStop.emit();
}
void
PlayQueue::play(std::size_t pos)
{
updateCurrentTrack(false);
Database::IdType trackId;
{
Wt::Dbo::Transaction transaction(LmsApp->getDboSession());
auto tracklist = getTrackList();
// If out of range, stop playing
if (pos >= tracklist->getCount())
{
stop();
return;
}
// If last and radio mode, fill the next song
if (_radioMode->checkState() == Wt::CheckState::Checked && pos == tracklist->getCount() - 1)
addRadioTrack();
_trackPos = pos;
auto track = tracklist->getEntry(*_trackPos)->getTrack();
trackId = track.id();
updateCurrentTrack(true);
if (!LmsApp->getUser()->isDemo())
LmsApp->getUser().modify()->setCurPlayingTrackPos(pos);
}
playTrack.emit(trackId);
}
void
PlayQueue::playPrevious()
{
if (!_trackPos)
return;
if (*_trackPos == 0)
stop();
else
play(*_trackPos - 1);
}
void
PlayQueue::playNext()
{
if (!_trackPos)
{
play(0);
return;
}
play(*_trackPos + 1);
}
void
PlayQueue::updateInfo()
{
Wt::Dbo::Transaction transaction(LmsApp->getDboSession());
_nbTracks->setText(Wt::WString::tr("Lms.PlayQueue.nb-tracks").arg(static_cast<unsigned>(getTrackList()->getCount())));
}
void
PlayQueue::updateCurrentTrack(bool selected)
{
if (!_trackPos || *_trackPos >= static_cast<std::size_t>(_entriesContainer->count()))
return;
auto track = _entriesContainer->widget(*_trackPos);
if (track)
{
if (selected)
track->addStyleClass("Lms-playqueue-selected");
else
track->removeStyleClass("Lms-playqueue-selected");
}
}
void
PlayQueue::enqueueTracks(const std::vector<Database::Track::pointer>& tracks)
{
// Use a "session" playqueue in order to store the current playqueue
// so that the user can disconnect and get its playqueue back
auto tracklist = getTrackList();
for (auto track : tracks)
Database::TrackListEntry::create(LmsApp->getDboSession(), track, tracklist);
updateInfo();
addSome();
}
void
PlayQueue::enqueueTrack(Database::Track::pointer track)
{
enqueueTracks(std::vector<Database::Track::pointer>(1, track));
}
void
PlayQueue::addTracks(const std::vector<Database::Track::pointer>& tracks)
{
enqueueTracks(tracks);
LmsApp->notifyMsg(MsgType::Info, Wt::WString::tr("Lms.PlayQueue.nb-tracks-added").arg(tracks.size()), std::chrono::milliseconds(2000));
}
void
PlayQueue::playTracks(const std::vector<Database::Track::pointer>& tracks)
{
Wt::Dbo::Transaction transaction(LmsApp->getDboSession());
clearTracks();
enqueueTracks(tracks);
play(0);
LmsApp->notifyMsg(MsgType::Info, Wt::WString::tr("Lms.PlayQueue.nb-tracks-playing").arg(tracks.size()), std::chrono::milliseconds(2000));
}
void
PlayQueue::addSome()
{
Wt::Dbo::Transaction transaction (LmsApp->getDboSession());
auto tracklist = getTrackList();
auto tracklistEntries = tracklist->getEntries(_entriesContainer->count(), 50);
for (auto tracklistEntry : tracklistEntries)
{
auto tracklistEntryId = tracklistEntry.id();
auto track = tracklistEntry->getTrack();
Wt::WTemplate* entry = _entriesContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.PlayQueue.template.entry"));
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
auto artist = track->getArtist();
if (artist)
{
entry->setCondition("if-has-artist", true);
entry->bindWidget("artist-name", LmsApplication::createArtistAnchor(track->getArtist()));
}
auto release = track->getRelease();
if (release)
{
entry->setCondition("if-has-release", true);
entry->bindWidget("release-name", LmsApplication::createReleaseAnchor(track->getRelease()));
}
Wt::WText* playBtn = entry->bindNew<Wt::WText>("play-btn", Wt::WString::tr("Lms.PlayQueue.play"), Wt::TextFormat::XHTML);
playBtn->clicked().connect(std::bind([=]
{
auto pos = _entriesContainer->indexOf(entry);
if (pos >= 0)
play(pos);
}));
Wt::WText* delBtn = entry->bindNew<Wt::WText>("del-btn", Wt::WString::tr("Lms.PlayQueue.delete"), Wt::TextFormat::XHTML);
delBtn->clicked().connect(std::bind([=]
{
// Remove the entry n both the widget tree and the playqueue
{
Wt::Dbo::Transaction transaction (LmsApp->getDboSession());
auto entryToRemove = Database::TrackListEntry::getById(LmsApp->getDboSession(), tracklistEntryId);
entryToRemove.remove();
}
if (_trackPos)
{
auto pos = _entriesContainer->indexOf(entry);
if (pos > 0 && *_trackPos >= static_cast<std::size_t>(pos))
(*_trackPos)--;
}
_entriesContainer->removeWidget(entry);
updateInfo();
}));
}
_showMore->setHidden(static_cast<std::size_t>(_entriesContainer->count()) >= tracklist->getCount());
}
void
PlayQueue::addRadioTrack()
{
auto now = std::chrono::system_clock::now();
std::mt19937 randGenerator(std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count());
auto tracklist = getTrackList();
std::set<Database::IdType> trackIds;
{
auto ids = tracklist->getTrackIds();
trackIds = std::set<Database::IdType>(ids.begin(), ids.end());
}
// Get all the tracks of the tracklist, get the cluster that is mostly used
// and reuse it to get the next track
auto clusters = tracklist->getClusters();
if (clusters.empty())
return;
for (auto cluster : clusters)
{
std::set<Database::IdType> clusterTrackIds = cluster->getTrackIds();
std::set<Database::IdType> candidateTrackIds;
std::set_difference(clusterTrackIds.begin(), clusterTrackIds.end(),
trackIds.begin(), trackIds.end(),
std::inserter(candidateTrackIds, candidateTrackIds.end()));
if (candidateTrackIds.empty())
continue;
std::uniform_int_distribution<int> dist(0, candidateTrackIds.size() - 1);
auto trackToAdd = Database::Track::getById(LmsApp->getDboSession(), *std::next(candidateTrackIds.begin(), dist(randGenerator)));
enqueueTrack(trackToAdd);
return;
}
LMS_LOG(UI, INFO) << "No more track to be added!";
}
} // namespace UserInterface
+79
View File
@@ -0,0 +1,79 @@
/*
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <Wt/WCheckBox.h>
#include <Wt/WContainerWidget.h>
#include <Wt/WPushButton.h>
#include <Wt/WSignal.h>
#include <Wt/WTemplate.h>
#include <Wt/WText.h>
#include <boost/optional.hpp>
#include "database/TrackList.hpp"
#include "database/Track.hpp"
namespace UserInterface {
class PlayQueue : public Wt::WTemplate
{
public:
PlayQueue();
void addTracks(const std::vector<Database::Track::pointer>& tracks);
void playTracks(const std::vector<Database::Track::pointer>& tracks);
// play the next track in the queue
void playNext();
// play the previous track in the queue
void playPrevious();
// Signal emitted when a track is to be played
Wt::Signal<Database::IdType> playTrack;
// Signal emitted when play has to be stopped
Wt::Signal<> playbackStop;
private:
Database::TrackList::pointer getTrackList();
void clearTracks();
void enqueueTracks(const std::vector<Database::Track::pointer>& tracks);
void enqueueTrack(Database::Track::pointer track);
void addSome();
void addRadioTrack();
void updateInfo();
void updateCurrentTrack(bool selected);
void play(std::size_t pos);
void stop();
boost::optional<Database::IdType> _tracklistId;
Wt::WCheckBox* _radioMode;
Wt::WContainerWidget* _entriesContainer;
Wt::WPushButton* _showMore;
Wt::WText* _nbTracks;
boost::optional<std::size_t> _trackPos; // current track position, if set
};
} // namespace UserInterface
+333
View File
@@ -0,0 +1,333 @@
/*
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "SettingsView.hpp"
#include <Wt/WString.h>
#include <Wt/WPushButton.h>
#include <Wt/WComboBox.h>
#include <Wt/WLineEdit.h>
#include <Wt/WFormModel.h>
#include <Wt/WStringListModel.h>
#include "common/Validators.hpp"
#include "utils/Logger.hpp"
#include "LmsApplication.hpp"
namespace UserInterface {
using namespace Database;
class SettingsModel : public Wt::WFormModel
{
public:
// Associate each field with a unique string literal.
static const Field BitrateField;
static const Field EncodingField;
static const Field PasswordField;
static const Field PasswordConfirmField;
SettingsModel()
: Wt::WFormModel()
{
initializeModels();
addField(BitrateField);
addField(EncodingField);
addField(PasswordField);
addField(PasswordConfirmField);
setValidator(BitrateField, createMandatoryValidator());
setValidator(EncodingField, createMandatoryValidator());
loadData();
}
std::shared_ptr<Wt::WAbstractItemModel> bitrateModel() { return _bitrateModel; }
std::shared_ptr<Wt::WAbstractItemModel> encodingModel() { return _encodingModel; }
void loadData()
{
Wt::Dbo::Transaction transaction(LmsApp->getDboSession());
auto bitrate = getBitrateRow(LmsApp->getUser()->getAudioBitrate());
if (bitrate)
setValue(BitrateField, bitrateString(*bitrate));
auto encodingRow = getEncodingRow(LmsApp->getUser()->getAudioEncoding());
if (encodingRow)
setValue(EncodingField, encodingString(*encodingRow));
}
void saveData()
{
Wt::Dbo::Transaction transaction(LmsApp->getDboSession());
auto bitrateRow = getBitrateRow(Wt::asString(value(BitrateField)));
assert(bitrateRow);
LmsApp->getUser().modify()->setAudioBitrate(bitrate(*bitrateRow));
auto encodingRow = getEncodingRow(Wt::asString(value(EncodingField)));
LmsApp->getUser().modify()->setAudioEncoding(encoding(*encodingRow));
if (!valueText(PasswordField).empty())
{
Database::Handler::getPasswordService().updatePassword(LmsApp->getAuthUser(), valueText(PasswordField));
}
}
bool validateField(Field field)
{
Wt::WString error;
if (field == PasswordField)
{
if (!valueText(PasswordField).empty())
{
// Evaluate the strength of the password
auto res = Database::Handler::getPasswordService().strengthValidator()->evaluateStrength(valueText(PasswordField), LmsApp->getUserIdentity(), "");
if (!res.isValid())
error = res.message();
}
else
return Wt::WFormModel::validateField(field);
}
else if (field == PasswordConfirmField)
{
if (validation(PasswordField).state() == Wt::ValidationState::Valid)
{
if (valueText(PasswordField) != valueText(PasswordConfirmField))
error = Wt::WString::tr("Lms.passwords-dont-match");
}
}
else
{
return Wt::WFormModel::validateField(field);
}
setValidation(field, Wt::WValidator::Result( error.empty() ? Wt::ValidationState::Valid : Wt::ValidationState::Invalid, error));
return (validation(field).state() == Wt::ValidationState::Valid);
}
boost::optional<int> getBitrateRow(Wt::WString value)
{
for (int i = 0; i < _bitrateModel->rowCount(); ++i)
{
if (bitrateString(i) == value)
return i;
}
return boost::none;
}
boost::optional<int> getBitrateRow(std::size_t value)
{
for (int i = 0; i < _bitrateModel->rowCount(); ++i)
{
if (bitrate(i) == value)
return i;
}
return boost::none;
}
std::size_t bitrate(int row)
{
return Wt::cpp17::any_cast<std::size_t>
(_bitrateModel->data(_bitrateModel->index(row, 0), Wt::ItemDataRole::User));
}
Wt::WString bitrateString(int row)
{
return Wt::cpp17::any_cast<Wt::WString>
(_bitrateModel->data(_bitrateModel->index(row, 0), Wt::ItemDataRole::Display));
}
boost::optional<int> getEncodingRow(Wt::WString value)
{
for (int i = 0; i < _encodingModel->rowCount(); ++i)
{
if (encodingString(i) == value)
return i;
}
return boost::none;
}
boost::optional<int> getEncodingRow(Database::AudioEncoding enc)
{
for (int i = 0; i < _encodingModel->rowCount(); ++i)
{
if (encoding(i) == enc)
return i;
}
return boost::none;
}
Database::AudioEncoding encoding(int row)
{
return Wt::cpp17::any_cast<Database::AudioEncoding>
(_encodingModel->data(_encodingModel->index(row, 0), Wt::ItemDataRole::User));
}
Wt::WString encodingString(int row)
{
return Wt::cpp17::any_cast<Wt::WString>
(_encodingModel->data(_encodingModel->index(row, 0), Wt::ItemDataRole::Display));
}
private:
void initializeModels()
{
_bitrateModel = std::make_shared<Wt::WStringListModel>();
std::size_t maxAudioBitrate;
{
Wt::Dbo::Transaction transaction(LmsApp->getDboSession());
maxAudioBitrate = LmsApp->getUser()->getMaxAudioBitrate();
}
std::size_t id = 0;
for (std::size_t bitrate : Database::User::audioBitrates)
{
if (bitrate > maxAudioBitrate)
break;
_bitrateModel->addString( Wt::WString::fromUTF8(std::to_string(bitrate / 1000)) );
_bitrateModel->setData( id, 0, bitrate, Wt::ItemDataRole::User);
id++;
}
_encodingModel = std::make_shared<Wt::WStringListModel>();
_encodingModel->addString(Wt::WString::tr("Lms.Settings.auto"));
_encodingModel->setData(0, 0, Database::AudioEncoding::AUTO, Wt::ItemDataRole::User);
_encodingModel->addString(Wt::WString::tr("Lms.Settings.mp3"));
_encodingModel->setData(1, 0, Database::AudioEncoding::MP3, Wt::ItemDataRole::User);
_encodingModel->addString(Wt::WString::tr("Lms.Settings.oga"));
_encodingModel->setData(2, 0, Database::AudioEncoding::OGA, Wt::ItemDataRole::User);
_encodingModel->addString(Wt::WString::tr("Lms.Settings.webma"));
_encodingModel->setData(3, 0, Database::AudioEncoding::WEBMA, Wt::ItemDataRole::User);
}
std::shared_ptr<Wt::WStringListModel> _bitrateModel;
std::shared_ptr<Wt::WStringListModel> _encodingModel;
};
const Wt::WFormModel::Field SettingsModel::BitrateField = "bitrate";
const Wt::WFormModel::Field SettingsModel::EncodingField = "encoding";
const Wt::WFormModel::Field SettingsModel::PasswordField = "password";
const Wt::WFormModel::Field SettingsModel::PasswordConfirmField = "password-confirm";
SettingsView::SettingsView()
{
wApp->internalPathChanged().connect(std::bind([=]
{
refreshView();
}));
refreshView();
}
void
SettingsView::refreshView()
{
if (!wApp->internalPathMatches("/settings"))
return;
clear();
auto t = addNew<Wt::WTemplateFormView>(Wt::WString::tr("Lms.Settings.template"));
auto model = std::make_shared<SettingsModel>();
// Password
auto password = std::make_unique<Wt::WLineEdit>();
password->setEchoMode(Wt::EchoMode::Password);
t->setFormWidget(SettingsModel::PasswordField, std::move(password));
// Password confirm
auto passwordConfirm = std::make_unique<Wt::WLineEdit>();
passwordConfirm->setEchoMode(Wt::EchoMode::Password);
t->setFormWidget(SettingsModel::PasswordConfirmField, std::move(passwordConfirm));
// Bitrate
auto bitrate = std::make_unique<Wt::WComboBox>();
bitrate->setModel(model->bitrateModel());
t->setFormWidget(SettingsModel::BitrateField, std::move(bitrate));
// Encoding
auto encoding = std::make_unique<Wt::WComboBox>();
encoding->setModel(model->encodingModel());
t->setFormWidget(SettingsModel::EncodingField, std::move(encoding));
// Buttons
Wt::WPushButton *saveBtn = t->bindWidget("apply-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.apply")));
Wt::WPushButton *discardBtn = t->bindWidget("discard-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.discard")));
saveBtn->clicked().connect(std::bind([=] ()
{
{
Wt::Dbo::Transaction transaction(LmsApp->getDboSession());
if (LmsApp->getUser()->isDemo())
{
LmsApp->notifyMsg(MsgType::Warning, Wt::WString::tr("Lms.Settings.demo-cannot-save"));
return;
}
}
t->updateModel(model.get());
if (model->validate())
{
model->saveData();
LmsApp->notifyMsg(MsgType::Success, Wt::WString::tr("Lms.Settings.settings-saved"));
}
// Udate the view: Delete any validation message in the view, etc.
t->updateView(model.get());
}));
discardBtn->clicked().connect(std::bind([=] ()
{
model->loadData();
model->validate();
t->updateView(model.get());
}));
t->updateView(model.get());
}
} // namespace UserInterface
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2013 Emeric Poupon
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
@@ -17,17 +17,20 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <Wt/WValidator>
#pragma once
#include <Wt/WContainerWidget.h>
namespace UserInterface {
class DirectoryValidator : public Wt::WValidator
class SettingsView : public Wt::WContainerWidget
{
public:
DirectoryValidator(Wt::WObject *parent = 0);
Wt::WValidator::Result validate(const Wt::WString& input) const;
SettingsView();
private:
void refreshView();
};
} // namespace UserInterface
+316
View File
@@ -0,0 +1,316 @@
/*
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "DatabaseSettingsView.hpp"
#include <Wt/WString.h>
#include <Wt/WPushButton.h>
#include <Wt/WComboBox.h>
#include <Wt/WLineEdit.h>
#include <Wt/WTemplateFormView.h>
#include <Wt/WFormModel.h>
#include <Wt/WStringListModel.h>
#include "common/Validators.hpp"
#include "utils/Logger.hpp"
#include "utils/Utils.hpp"
#include "LmsApplication.hpp"
namespace UserInterface {
using namespace Database;
class DatabaseSettingsModel : public Wt::WFormModel
{
public:
// Associate each field with a unique string literal.
static const Field MediaDirectoryField;
static const Field UpdatePeriodField;
static const Field UpdateStartTimeField;
static const Field TagsField;
DatabaseSettingsModel()
: Wt::WFormModel()
{
initializeModels();
addField(MediaDirectoryField);
addField(UpdatePeriodField);
addField(UpdateStartTimeField);
addField(TagsField);
auto dirValidator = std::make_shared<DirectoryValidator>();
dirValidator->setMandatory(true);
setValidator(MediaDirectoryField, dirValidator);
setValidator(UpdatePeriodField, createMandatoryValidator());
setValidator(UpdateStartTimeField, createMandatoryValidator());
setValidator(TagsField, createTagsValidator());
// populate the model with initial data
loadData();
}
std::shared_ptr<Wt::WAbstractItemModel> updatePeriodModel() { return _updatePeriodModel; }
std::shared_ptr<Wt::WAbstractItemModel> updateStartTimeModel() { return _updateStartTimeModel; }
void loadData()
{
using namespace Database;
Wt::Dbo::Transaction transaction(LmsApp->getDboSession());
auto scanSettings = ScanSettings::get(LmsApp->getDboSession());
setValue(MediaDirectoryField, scanSettings->getMediaDirectory().string());
auto periodRow = getUpdatePeriodModelRow( scanSettings->getUpdatePeriod() );
if (periodRow)
setValue(UpdatePeriodField, updatePeriodString(*periodRow));
auto startTimeRow = getUpdateStartTimeModelRow( scanSettings->getUpdateStartTime() );
if (startTimeRow)
setValue(UpdateStartTimeField, updateStartTimeString(*startTimeRow) );
auto clusterTypes = scanSettings->getClusterTypes();
if (!clusterTypes.empty())
{
std::vector<std::string> names;
std::transform(clusterTypes.begin(), clusterTypes.end(),std::back_inserter(names), [](auto clusterType) { return clusterType->getName(); });
setValue(TagsField, joinStrings(names, " "));
}
}
void saveData()
{
Wt::Dbo::Transaction transaction(LmsApp->getDboSession());
auto scanSettings = ScanSettings::get(LmsApp->getDboSession());
scanSettings.modify()->setMediaDirectory(valueText(MediaDirectoryField).toUTF8());
auto updatePeriodRow = getUpdatePeriodModelRow( valueText(UpdatePeriodField));
assert(updatePeriodRow);
scanSettings.modify()->setUpdatePeriod(updatePeriod(*updatePeriodRow));
auto startTimeRow = getUpdateStartTimeModelRow( valueText(UpdateStartTimeField));
assert(startTimeRow);
scanSettings.modify()->setUpdateStartTime(updateStartTime(*startTimeRow));
auto clusterTypes = splitString(valueText(TagsField).toUTF8(), " ");
scanSettings.modify()->setClusterTypes(std::set<std::string>(clusterTypes.begin(), clusterTypes.end()));
}
boost::optional<int> getUpdatePeriodModelRow(Wt::WString value)
{
for (int i = 0; i < _updatePeriodModel->rowCount(); ++i)
{
if (updatePeriodString(i) == value)
return i;
}
return boost::none;
}
boost::optional<int> getUpdatePeriodModelRow(ScanSettings::UpdatePeriod period)
{
for (int i = 0; i < _updatePeriodModel->rowCount(); ++i)
{
if (updatePeriod(i) == period)
return i;
}
return boost::none;
}
ScanSettings::UpdatePeriod updatePeriod(int row)
{
return Wt::cpp17::any_cast<ScanSettings::UpdatePeriod>
(_updatePeriodModel->data(_updatePeriodModel->index(row, 0), Wt::ItemDataRole::User));
}
Wt::WString updatePeriodString(int row)
{
return Wt::cpp17::any_cast<Wt::WString>
(_updatePeriodModel->data(_updatePeriodModel->index(row, 0), Wt::ItemDataRole::Display));
}
boost::optional<int> getUpdateStartTimeModelRow(Wt::WString value)
{
for (int i = 0; i < _updateStartTimeModel->rowCount(); ++i)
{
if (updateStartTimeString(i) == value)
return i;
}
return boost::none;
}
boost::optional<int> getUpdateStartTimeModelRow(Wt::WTime startTime)
{
for (int i = 0; i < _updateStartTimeModel->rowCount(); ++i)
{
if (updateStartTime(i) == startTime)
return i;
}
return boost::none;
}
Wt::WTime updateStartTime(int row)
{
return Wt::cpp17::any_cast<Wt::WTime>
(_updateStartTimeModel->data(_updateStartTimeModel->index(row, 0), Wt::ItemDataRole::User));
}
Wt::WString updateStartTimeString(int row)
{
return Wt::cpp17::any_cast<Wt::WString>
(_updateStartTimeModel->data(_updateStartTimeModel->index(row, 0), Wt::ItemDataRole::Display));
}
private:
static std::shared_ptr<Wt::WValidator> createTagsValidator()
{
auto v = std::make_shared<Wt::WValidator>();
return v;
}
void initializeModels()
{
_updatePeriodModel = std::make_shared<Wt::WStringListModel>();
_updatePeriodModel->addString(Wt::WString::tr("Lms.Admin.Database.never"));
_updatePeriodModel->setData(0, 0, ScanSettings::UpdatePeriod::Never, Wt::ItemDataRole::User);
_updatePeriodModel->addString(Wt::WString::tr("Lms.Admin.Database.daily"));
_updatePeriodModel->setData(1, 0, ScanSettings::UpdatePeriod::Daily, Wt::ItemDataRole::User);
_updatePeriodModel->addString(Wt::WString::tr("Lms.Admin.Database.weekly"));
_updatePeriodModel->setData(2, 0, ScanSettings::UpdatePeriod::Weekly, Wt::ItemDataRole::User);
_updatePeriodModel->addString(Wt::WString::tr("Lms.Admin.Database.monthly"));
_updatePeriodModel->setData(3, 0, ScanSettings::UpdatePeriod::Monthly, Wt::ItemDataRole::User);
_updateStartTimeModel = std::make_shared<Wt::WStringListModel>();
for (std::size_t i = 0; i < 24; ++i)
{
Wt::WTime time(i, 0);
_updateStartTimeModel->addString( time.toString() );
_updateStartTimeModel->setData(i, 0, time, Wt::ItemDataRole::User);
}
}
std::shared_ptr<Wt::WStringListModel> _updatePeriodModel;
std::shared_ptr<Wt::WStringListModel> _updateStartTimeModel;
};
const Wt::WFormModel::Field DatabaseSettingsModel::MediaDirectoryField = "media-directory";
const Wt::WFormModel::Field DatabaseSettingsModel::UpdatePeriodField = "update-period";
const Wt::WFormModel::Field DatabaseSettingsModel::UpdateStartTimeField = "update-start-time";
const Wt::WFormModel::Field DatabaseSettingsModel::TagsField = "tags";
DatabaseSettingsView::DatabaseSettingsView()
{
wApp->internalPathChanged().connect(std::bind([=]
{
refreshView();
}));
refreshView();
}
void
DatabaseSettingsView::refreshView()
{
if (!wApp->internalPathMatches("/admin/database"))
return;
clear();
auto t = addNew<Wt::WTemplateFormView>(Wt::WString::tr("Lms.Admin.Database.template"));
auto model = std::make_shared<DatabaseSettingsModel>();
// Media Directory
t->setFormWidget(DatabaseSettingsModel::MediaDirectoryField, std::make_unique<Wt::WLineEdit>());
// Update Period
auto updatePeriod = std::make_unique<Wt::WComboBox>();
updatePeriod->setModel(model->updatePeriodModel());
t->setFormWidget(DatabaseSettingsModel::UpdatePeriodField, std::move(updatePeriod));
// Update Start Time
auto updateStartTime = std::make_unique<Wt::WComboBox>();
updateStartTime->setModel(model->updateStartTimeModel());
t->setFormWidget(DatabaseSettingsModel::UpdateStartTimeField, std::move(updateStartTime));
// Tags
t->setFormWidget(DatabaseSettingsModel::TagsField, std::make_unique<Wt::WLineEdit>());
// Buttons
Wt::WPushButton *saveBtn = t->bindWidget("apply-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.apply")));
Wt::WPushButton *discardBtn = t->bindWidget("discard-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.discard")));
Wt::WPushButton *immScanBtn = t->bindWidget("immediate-scan-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.Admin.Database.immediate-scan")));
saveBtn->clicked().connect(std::bind([=] ()
{
t->updateModel(model.get());
if (model->validate())
{
model->saveData();
LmsApp->getMediaScanner().reschedule();
LmsApp->notifyMsg(MsgType::Success, Wt::WString::tr("Lms.Admin.Database.settings-saved"));
}
// Udate the view: Delete any validation message in the view, etc.
t->updateView(model.get());
}));
discardBtn->clicked().connect(std::bind([=] ()
{
model->loadData();
model->validate();
t->updateView(model.get());
}));
immScanBtn->clicked().connect(std::bind([=] ()
{
LmsApp->getMediaScanner().scheduleImmediateScan();
LmsApp->notifyMsg(MsgType::Info, Wt::WString::tr("Lms.Admin.Database.scan-launched"));
}));
t->updateView(model.get());
}
} // namespace UserInterface
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2015 Emeric Poupon
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
@@ -19,19 +19,19 @@
#pragma once
#include <Wt/WContainerWidget>
#include <Wt/WContainerWidget.h>
namespace UserInterface {
namespace Mobile {
class ArtistSearchView : public Wt::WContainerWidget
class DatabaseSettingsView : public Wt::WContainerWidget
{
public:
DatabaseSettingsView();
ArtistSearchView(Wt::WContainerWidget* parent = 0);
private:
void refreshView();
};
} // namespace Mobile
} // namespace UserInterface
+154
View File
@@ -0,0 +1,154 @@
/*
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "InitWizardView.hpp"
#include <Wt/WFormModel.h>
#include <Wt/WLineEdit.h>
#include <Wt/WPushButton.h>
#include <Wt/Auth/Identity.h>
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
#include "common/Validators.hpp"
#include "LmsApplication.hpp"
namespace UserInterface {
class InitWizardModel : public Wt::WFormModel
{
public:
// Associate each field with a unique string literal.
static const Field AdminLoginField;
static const Field PasswordField;
static const Field PasswordConfirmField;
InitWizardModel() : Wt::WFormModel()
{
addField(AdminLoginField);
addField(PasswordField);
addField(PasswordConfirmField);
setValidator(AdminLoginField, createNameValidator());
setValidator(PasswordField, createMandatoryValidator());
setValidator(PasswordConfirmField, createMandatoryValidator());
}
void saveData()
{
Wt::Dbo::Transaction transaction(LmsApp->getDboSession());
// Check if a user already exist
// If it's the case, just do nothing
if (!Database::User::getAll(LmsApp->getDboSession()).empty())
throw LmsException("Admin user already created");
// Create user
Wt::Auth::User authUser = LmsApp->getDb().getUserDatabase().registerNew();
Database::User::pointer user = LmsApp->getDb().createUser(authUser);
// Account
authUser.setIdentity(Wt::Auth::Identity::LoginName, valueText(AdminLoginField));
Database::Handler::getPasswordService().updatePassword(authUser, valueText(PasswordField));
user.modify()->setType(Database::User::Type::ADMIN);
}
bool validateField(Field field)
{
Wt::WString error;
if (field == PasswordField)
{
if (!valueText(PasswordField).empty())
{
// Evaluate the strength of the password
auto res = Database::Handler::getPasswordService().strengthValidator()->evaluateStrength(valueText(PasswordField),
valueText(AdminLoginField), "");
if (!res.isValid())
error = res.message();
}
else
return Wt::WFormModel::validateField(field);
}
else if (field == PasswordConfirmField)
{
if (validation(PasswordField).state() == Wt::ValidationState::Valid)
{
if (valueText(PasswordField) != valueText(PasswordConfirmField))
error = Wt::WString::tr("Lms.passwords-dont-match");
}
}
else
{
return Wt::WFormModel::validateField(field);
}
setValidation(field, Wt::WValidator::Result( error.empty() ? Wt::ValidationState::Valid : Wt::ValidationState::Invalid, error));
return (validation(field).state() == Wt::ValidationState::Valid);
}
};
const Wt::WFormModel::Field InitWizardModel::AdminLoginField = "admin-login";
const Wt::WFormModel::Field InitWizardModel::PasswordField = "password";
const Wt::WFormModel::Field InitWizardModel::PasswordConfirmField = "password-confirm";
InitWizardView::InitWizardView()
: Wt::WTemplateFormView(Wt::WString::tr("Lms.Admin.InitWizard.template"))
{
auto model = std::make_shared<InitWizardModel>();
// AdminLogin
setFormWidget(InitWizardModel::AdminLoginField, std::make_unique<Wt::WLineEdit>());
// Password
auto passwordEdit = std::make_unique<Wt::WLineEdit>();
passwordEdit->setEchoMode(Wt::EchoMode::Password);
setFormWidget(InitWizardModel::PasswordField, std::move(passwordEdit) );
// Password confirmation
auto passwordConfirmEdit = std::make_unique<Wt::WLineEdit>();
passwordConfirmEdit->setEchoMode(Wt::EchoMode::Password);
setFormWidget(InitWizardModel::PasswordConfirmField, std::move(passwordConfirmEdit));
Wt::WPushButton* saveButton = bindNew<Wt::WPushButton>("create-btn", Wt::WString::tr("Lms.create"));
saveButton->clicked().connect(std::bind([=]
{
updateModel(model.get());
if (model->validate())
{
model->saveData();
LmsApp->notifyMsg(MsgType::Success, Wt::WString::tr("Lms.Admin.InitWizard.done"));
saveButton->setEnabled(false);
}
updateView(model.get());
}));
updateView(model.get());
}
} // namespace UserInterface
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2016 Emeric Poupon
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
@@ -19,19 +19,17 @@
#pragma once
#include <Wt/WContainerWidget>
#include <Wt/WTemplateFormView.h>
namespace UserInterface {
namespace Mobile {
class ReleaseSearchView : public Wt::WContainerWidget
class InitWizardView : public Wt::WTemplateFormView
{
public:
ReleaseSearchView(Wt::WContainerWidget* parent = 0);
InitWizardView();
};
} // namespace Mobile
} // namespace UserInterface
+323
View File
@@ -0,0 +1,323 @@
/*
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "UserView.hpp"
#include <Wt/WApplication.h>
#include <Wt/WCheckBox.h>
#include <Wt/WComboBox.h>
#include <Wt/WLineEdit.h>
#include <Wt/WPushButton.h>
#include <Wt/WTemplateFormView.h>
#include <Wt/WFormModel.h>
#include <Wt/WStringListModel.h>
#include "common/Validators.hpp"
#include "utils/Config.hpp"
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
#include "utils/Utils.hpp"
#include "LmsApplication.hpp"
namespace UserInterface {
class UserModel : public Wt::WFormModel
{
public:
static const Field LoginField;
static const Field PasswordField;
static const Field BitrateLimitField;
static const Field DemoField;
UserModel(boost::optional<Database::IdType> userId)
: Wt::WFormModel(),
_userId(userId)
{
if (!_userId)
{
addField(LoginField);
setValidator(LoginField, createNameValidator());
}
addField(PasswordField);
addField(BitrateLimitField);
addField(DemoField);
if (!_userId)
setValidator(PasswordField, createMandatoryValidator());
initializeModels();
// populate the model with initial data
loadData();
}
void loadData()
{
if (!_userId)
return;
Wt::Dbo::Transaction transaction(LmsApp->getDboSession());
auto authUser = LmsApp->getDb().getUserDatabase().findWithId( std::to_string(*_userId) );
auto user = LmsApp->getDb().getUser(authUser);
if (user == LmsApp->getUser())
throw LmsException("Cannot edit ourselves");
auto bitrate = getBitrateLimitRow(user->getMaxAudioBitrate());
if (bitrate)
setValue(BitrateLimitField, bitrateLimitString(*bitrate));
}
void saveData()
{
Wt::Dbo::Transaction transaction(LmsApp->getDboSession());
if (_userId)
{
// Update user
Wt::Auth::User authUser = LmsApp->getDb().getUserDatabase().findWithId( std::to_string(*_userId) );
Database::User::pointer user = LmsApp->getDb().getUser( authUser );
// Account
if (!valueText(PasswordField).empty())
Database::Handler::getPasswordService().updatePassword(authUser, valueText(PasswordField));
auto bitrateLimitRow = getBitrateLimitRow(Wt::asString(value(BitrateLimitField)));
user.modify()->setMaxAudioBitrate(bitrateLimit(*bitrateLimitRow));
LMS_LOG(UI, DEBUG) << "Max audio bitrate set to " << bitrateLimit(*bitrateLimitRow);
}
else
{
// Create user
Wt::Auth::User authUser = LmsApp->getDb().getUserDatabase().registerNew();
Database::User::pointer user = LmsApp->getDb().createUser(authUser);
// Account
authUser.setIdentity(Wt::Auth::Identity::LoginName, valueText(LoginField));
Database::Handler::getPasswordService().updatePassword(authUser, valueText(PasswordField));
auto bitrateLimitRow = getBitrateLimitRow(Wt::asString(value(BitrateLimitField)));
user.modify()->setMaxAudioBitrate(bitrateLimit(*bitrateLimitRow));
if (Wt::asNumber(value(DemoField)))
user.modify()->setType(Database::User::Type::DEMO);
}
}
Wt::WString getLogin() const
{
if (_userId)
{
auto authUser = LmsApp->getDb().getUserDatabase().findWithId( std::to_string(*_userId) );
return authUser.identity(Wt::Auth::Identity::LoginName);
}
else
return valueText(LoginField);
}
bool validateField(Field field)
{
Wt::WString error;
if (field == LoginField)
{
auto user = LmsApp->getDb().getUserDatabase().findWithIdentity(Wt::Auth::Identity::LoginName, valueText(LoginField));
if (user.isValid())
error = Wt::WString::tr("Lms.Admin.User.user-already-exists");
}
else if (field == PasswordField)
{
if (!valueText(PasswordField).empty())
{
if (Wt::asNumber(value(DemoField)))
{
//Demo account: password must be the same as the login name
if (valueText(PasswordField) != getLogin())
error = Wt::WString::tr("Lms.Admin.User.demo-password-invalid");
}
else
{
// Evaluate the strength of the password for non demo accounts
auto res = Database::Handler::getPasswordService().strengthValidator()->evaluateStrength(valueText(PasswordField), getLogin(), "");
if (!res.isValid())
error = res.message();
}
}
}
else if (field == DemoField)
{
Wt::Dbo::Transaction transaction(LmsApp->getDboSession());
if (Wt::asNumber(value(DemoField)) && Database::User::getDemo(LmsApp->getDboSession()))
error = Wt::WString::tr("Lms.Admin.User.demo-account-already-exists");
}
if (error.empty())
return Wt::WFormModel::validateField(field);
setValidation(field, Wt::WValidator::Result( Wt::ValidationState::Invalid, error));
return false;
}
boost::optional<int> getBitrateLimitRow(Wt::WString value)
{
for (int i = 0; i < _bitrateModel->rowCount(); ++i)
{
if (bitrateLimitString(i) == value)
return i;
}
return boost::none;
}
boost::optional<int> getBitrateLimitRow(std::size_t value)
{
for (int i = 0; i < _bitrateModel->rowCount(); ++i)
{
if (bitrateLimit(i) == value)
return i;
}
return boost::none;
}
std::size_t bitrateLimit(int row)
{
return Wt::cpp17::any_cast<std::size_t>
(_bitrateModel->data(_bitrateModel->index(row, 0), Wt::ItemDataRole::User));
}
Wt::WString bitrateLimitString(int row)
{
return Wt::cpp17::any_cast<Wt::WString>
(_bitrateModel->data(_bitrateModel->index(row, 0), Wt::ItemDataRole::Display));
}
std::shared_ptr<Wt::WAbstractItemModel> bitrateModel() { return _bitrateModel; }
private:
void initializeModels()
{
_bitrateModel = std::make_shared<Wt::WStringListModel>();
std::size_t id = 0;
for (auto bitrate : Database::User::audioBitrates)
{
_bitrateModel->addString( Wt::WString::fromUTF8(std::to_string(bitrate / 1000)) );
_bitrateModel->setData( id++, 0, bitrate, Wt::ItemDataRole::User);
}
}
std::shared_ptr<Wt::WStringListModel> _bitrateModel;
boost::optional<Database::IdType> _userId;
};
const Wt::WFormModel::Field UserModel::LoginField = "login";
const Wt::WFormModel::Field UserModel::PasswordField = "password";
const Wt::WFormModel::Field UserModel::BitrateLimitField = "audio-bitrate-limit";
const Wt::WFormModel::Field UserModel::DemoField = "demo";
UserView::UserView()
{
wApp->internalPathChanged().connect(std::bind([=]
{
refreshView();
}));
refreshView();
}
void
UserView::refreshView()
{
if (!wApp->internalPathMatches("/admin/user"))
return;
auto userId = readAs<Database::IdType>(wApp->internalPathNextPart("/admin/user/"));
LMS_LOG(UI, DEBUG) << "userId = " << (userId ? std::to_string(*userId) : "none");
clear();
Wt::WTemplateFormView* t = addNew<Wt::WTemplateFormView>(Wt::WString::tr("Lms.Admin.User.template"));
auto model = std::make_shared<UserModel>(userId);
if (userId)
{
auto authUser = LmsApp->getDb().getUserDatabase().findWithId( std::to_string(*userId) );
auto name = authUser.identity(Wt::Auth::Identity::LoginName);
t->bindString("title", Wt::WString::tr("Lms.Admin.User.user-edit").arg(name), Wt::TextFormat::Plain);
}
else
{
// Login
t->setCondition("if-has-login", true);
t->setFormWidget(UserModel::LoginField, std::make_unique<Wt::WLineEdit>());
t->bindString("title", Wt::WString::tr("Lms.Admin.User.user-create"));
}
// Password
auto passwordEdit = std::make_unique<Wt::WLineEdit>();
passwordEdit->setEchoMode(Wt::EchoMode::Password);
t->setFormWidget(UserModel::PasswordField, std::move(passwordEdit));
// Bitrate
auto bitrate = std::make_unique<Wt::WComboBox>();
bitrate->setModel(model->bitrateModel());
t->setFormWidget(UserModel::BitrateLimitField, std::move(bitrate));
// Demo account
t->setFormWidget(UserModel::DemoField, std::make_unique<Wt::WCheckBox>());
if (!userId && Config::instance().getBool("demo", false))
t->setCondition("if-demo", true);
Wt::WPushButton* saveBtn = t->bindNew<Wt::WPushButton>("save-btn", Wt::WString::tr(userId ? "Lms.save" : "Lms.create"));
saveBtn->clicked().connect(std::bind([=]
{
t->updateModel(model.get());
if (model->validate())
{
model->saveData();
LmsApp->notifyMsg(MsgType::Success, Wt::WString::tr(userId ? "Lms.Admin.User.user-updated" : "Lms.Admin.User.user-created"));
LmsApp->setInternalPath("/admin/users", true);
}
else
{
t->updateView(model.get());
}
}));
t->updateView(model.get());
}
} // namespace UserInterface
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2016 Emeric Poupon
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
@@ -19,18 +19,19 @@
#pragma once
#include <Wt/WSignal>
#include "database/Types.hpp"
#include <Wt/WContainerWidget.h>
namespace UserInterface {
namespace Mobile {
struct PlayQueueEvents
class UserView : public Wt::WContainerWidget
{
Wt::Signal<Database::Track::id_type> trackPlay;
Wt::Signal<Database::Track::id_type> trackAdd;
public:
UserView();
private:
void refreshView();
};
} // namespace Mobile
} // namespace UserInterface
+127
View File
@@ -0,0 +1,127 @@
/*
* Copyright (C) 2018 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "UsersView.hpp"
#include <Wt/WPushButton.h>
#include <Wt/WMessageBox.h>
#include <Wt/WTemplate.h>
#include "database/User.hpp"
#include "utils/Logger.hpp"
#include "LmsApplication.hpp"
namespace UserInterface {
UsersView::UsersView()
: Wt::WTemplate(Wt::WString::tr("Lms.Admin.Users.template"))
{
addFunction("tr", &Wt::WTemplate::Functions::tr);
_container = bindNew<Wt::WContainerWidget>("users");
Wt::WPushButton* addBtn = bindNew<Wt::WPushButton>("add-btn", Wt::WString::tr("Lms.Admin.Users.add"));
addBtn->clicked().connect(std::bind([=]
{
LmsApp->setInternalPath("/admin/user", true);
}));
wApp->internalPathChanged().connect(std::bind([=]
{
refreshView();
}));
refreshView();
}
void
UsersView::refreshView()
{
if (!wApp->internalPathMatches("/admin/users"))
return;
_container->clear();
Wt::Dbo::Transaction transaction(LmsApp->getDboSession());
auto users = Database::User::getAll(LmsApp->getDboSession());
for (auto user : users)
{
auto userId = std::to_string(user.id());
Wt::WTemplate* entry = _container->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Admin.Users.template.entry"));
Wt::Auth::User authUser = LmsApp->getDb().getUserDatabase().findWithId(userId);
if (!authUser.isValid()) {
LMS_LOG(UI, ERROR) << "Skipping invalid userId = " << user.id();
continue;
}
auto login = authUser.identity(Wt::Auth::Identity::LoginName);
entry->bindString("name", login, Wt::TextFormat::Plain);
// Create tag
if (user->isAdmin() || user->isDemo())
{
entry->setCondition("if-tag", true);
entry->bindString("tag", Wt::WString::tr(user->isAdmin() ? "Lms.Admin.Users.admin" : "Lms.Admin.Users.demo"));
}
// Don't edit ourself this way
if (LmsApp->getUser() == user)
continue;
entry->setCondition("if-edit", true);
Wt::WPushButton* editBtn = entry->bindNew<Wt::WPushButton>("edit-btn", Wt::WString::tr("Lms.Admin.Users.edit"));
editBtn->clicked().connect(std::bind([=]
{
LmsApp->setInternalPath("/admin/user/" + std::to_string(user.id()), true);
}));
Wt::WPushButton* delBtn = entry->bindNew<Wt::WPushButton>("del-btn", Wt::WString::tr("Lms.Admin.Users.del"));
delBtn->clicked().connect([=]
{
auto msgBox = delBtn->addChild(std::make_unique<Wt::WMessageBox>(Wt::WString::tr("Lms.Admin.Users.del-user"),
Wt::WString::tr("Lms.Admin.Users.del-user-name"),
Wt::Icon::Warning, Wt::StandardButton::Yes | Wt::StandardButton::No));
msgBox->setModal(true);
msgBox->buttonClicked().connect([=] (Wt::StandardButton btn)
{
if (btn == Wt::StandardButton::Yes)
{
Wt::Dbo::Transaction transaction(LmsApp->getDboSession());
auto authUser = LmsApp->getDb().getUserDatabase().findWithId(userId);
auto user = LmsApp->getDb().getUser(authUser);
LmsApp->getDb().getUserDatabase().deleteUser( authUser );
user.remove();
_container->removeWidget(entry);
}
delBtn->removeChild(msgBox);
});
msgBox->show();
});
}
}
} // namespace UserInterface

Some files were not shown because too many files have changed in this diff Show More