Merge branch 'develop'
This commit is contained in:
+32
-9
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
@@ -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
@@ -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
@@ -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">×</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
File diff suppressed because one or more lines are too long
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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; }
|
||||
|
||||
|
||||
@@ -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>();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user