Merge branch 'develop'
This commit is contained in:
@@ -49,6 +49,15 @@ $ /usr/bin/lms --docroot='/usr/share/lms/docroot/;/resources,/css,/images' --app
|
|||||||
It is highly recommended to run LMS as a non root user.
|
It is highly recommended to run LMS as a non root user.
|
||||||
The exectuable needs write accesses to the /var/lms/ directory.
|
The exectuable needs write accesses to the /var/lms/ directory.
|
||||||
|
|
||||||
|
## 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)
|
## Setting up SSL materials (optional)
|
||||||
Here is just a self signed certificate example, you could do use a CA if you want.
|
Here is just a self signed certificate example, you could do use a CA if you want.
|
||||||
|
|
||||||
@@ -70,4 +79,5 @@ Depending on your SSL parameters, you may be asked for the PEM passphrase to unl
|
|||||||
|
|
||||||
- Wt (http://www.webtoolkit.eu/)
|
- Wt (http://www.webtoolkit.eu/)
|
||||||
- bootstrap3 (http://getbootstrap.com/)
|
- bootstrap3 (http://getbootstrap.com/)
|
||||||
- libav project (https://www.libav.org/).
|
- libav project (https://www.libav.org/)
|
||||||
|
- Magick++ (http://www.imagemagick.org/Magick++/)
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
|
||||||
[Book]
|
[Book]
|
||||||
- Make the feature
|
- Make the feature?
|
||||||
|
|
||||||
[Cover]
|
[Cover]
|
||||||
- Handle preferred cover file names
|
- 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 and a grabber from some web service (mandatory for artists?)
|
||||||
|
|
||||||
[Database]
|
[Database]
|
||||||
- Optim, use SQL query to get the "genre" orphans
|
- Optim, use SQL query to get the "genre" orphans
|
||||||
@@ -13,7 +13,6 @@
|
|||||||
- add a global play counter for tracks. This will help people to spot most popular files
|
- 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
|
- 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 the WServer::post method to notify the end of the database scan? (with results?)
|
||||||
- Put more config information in the database, access from the UI
|
|
||||||
|
|
||||||
[Metadata]
|
[Metadata]
|
||||||
- OGG metadata -> properly handle metadata nested in the audio stream
|
- OGG metadata -> properly handle metadata nested in the audio stream
|
||||||
@@ -30,9 +29,11 @@
|
|||||||
- consider adding support for external web servers (new FCGI service?)
|
- consider adding support for external web servers (new FCGI service?)
|
||||||
|
|
||||||
[UI]
|
[UI]
|
||||||
- handle internationalization
|
- Handle internationalization
|
||||||
|
- Handle internal path
|
||||||
|
- Cannot log again when refreshing the application in firefox (have to close/open a new browser tab)
|
||||||
[Settings]
|
[Settings]
|
||||||
- logout users that are being changed (loss of admin admin rights), or make sure they are still admin when they make changes
|
- 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?
|
- "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
|
- add a scroll area in order for settings to work on mobile devices
|
||||||
[user/transcoding]
|
[user/transcoding]
|
||||||
@@ -46,23 +47,28 @@
|
|||||||
- refresh the audio/video view when the database directories have been updated
|
- refresh the audio/video view when the database directories have been updated
|
||||||
|
|
||||||
[Audio]
|
[Audio]
|
||||||
- Style eveything nicely...
|
- drop the winamp like interface?
|
||||||
- MediaPlayer: move the slider using js (http://redmine.webtoolkit.eu/boards/2/topics/7924?r=8478, http://redmine.emweb.be/boards/2/topics/10994)
|
- Add a download button to get the current playlist in a streamed zip file
|
||||||
- TrackView: Reselect the current selected item when displaying the updated search results
|
- Add a upload button to upload media files in a dedicated directory
|
||||||
- Add covers in the release filter?
|
- Perform a search for each key pressed (using a timer, in JS)
|
||||||
- Add a download button to get the current playlist in a streamed zip file
|
- Save user prefs in local storage (volume, loop, shuffle, current playqueue, etc.)
|
||||||
- Add a upload button to upload media files in a dedicated directory
|
[desktop]
|
||||||
- Add keyboard shortcuts
|
- Better style...
|
||||||
|
- TrackView: Reselect the current selected item when displaying the updated search results
|
||||||
|
- Add keyboard shortcuts
|
||||||
|
[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]
|
[Video]
|
||||||
- implement a decent mediaplayer
|
- implement a decent mediaplayer
|
||||||
- remove codec choice from parameters
|
- remove codec choice from parameters
|
||||||
|
|
||||||
[Layout]
|
|
||||||
- Style eveything nicely...
|
|
||||||
- Make a dedicated audio layout in mobile environment (keep the same layout for videos?)
|
|
||||||
- Handle internal path
|
|
||||||
|
|
||||||
[REST API]
|
[REST API]
|
||||||
- Make a dedicated REST API. Maybe use the SubSonic API or Ampache API?
|
- Make a dedicated REST API. Maybe use the SubSonic API or Ampache API?
|
||||||
|
|
||||||
|
|||||||
+124
-20
@@ -2,6 +2,18 @@
|
|||||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||||
<!--FORMS message blocks-->
|
<!--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">
|
<message id="firstConnectionForm-template">
|
||||||
<legend>${title}</legend>
|
<legend>${title}</legend>
|
||||||
<div class="form-horizontal">
|
<div class="form-horizontal">
|
||||||
@@ -419,29 +431,121 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</message>
|
</message>
|
||||||
|
|
||||||
<message id="mobile-audio-player">
|
<message id="wa-release-wrapper">
|
||||||
<div class="container">
|
<div class="section text-center">
|
||||||
<div class="row">
|
<div class="container">
|
||||||
<div class="col-xs-12">
|
<div class="row">
|
||||||
<div class="row">
|
${contents}
|
||||||
<div class="mobile-audio-footer">
|
|
||||||
<div class="vertical-align mobile-audio-footer">
|
|
||||||
<div class="col-xs-2">${cover}</div>
|
|
||||||
<div class="col-xs-7">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-12">${track}</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-12">${artist}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-3">${play}${pause}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</message>
|
</message>
|
||||||
|
|
||||||
|
<message id="wa-release-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>
|
||||||
|
|
||||||
|
<message id="wa-artist-wrapper">
|
||||||
|
<div class="section text-center">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
${contents}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</message>
|
||||||
|
|
||||||
|
<message id="wa-artist-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>
|
||||||
|
|
||||||
|
<message id="wa-track-wrapper">
|
||||||
|
${title}
|
||||||
|
<div class="menu-category list-group ">
|
||||||
|
${track-container}
|
||||||
|
</div>
|
||||||
|
${show-more}
|
||||||
|
</message>
|
||||||
|
|
||||||
|
<message id="wa-track">
|
||||||
|
<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" style="margin-top:3px"><span class="badge">${btn}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</message>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Track container
|
||||||
|
|
||||||
|
Track bar title
|
||||||
|
|
||||||
|
Release1
|
||||||
|
Track1
|
||||||
|
Track2
|
||||||
|
...
|
||||||
|
|
||||||
|
Release2
|
||||||
|
...
|
||||||
|
|
||||||
|
...
|
||||||
|
-->
|
||||||
|
<message id="wa-trackview-wrapper">
|
||||||
|
${title}
|
||||||
|
${release-container}
|
||||||
|
${show-more}
|
||||||
|
</message>
|
||||||
|
|
||||||
|
<message id="wa-trackview-release-container">
|
||||||
|
<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>
|
||||||
|
</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} <span class="badge">${btn}</span><span class="badge">${time}</span></div>
|
||||||
|
</message>
|
||||||
|
|
||||||
|
<message id="wa-audio-player">
|
||||||
|
${audio}
|
||||||
|
${prev}${play-pause}${next}
|
||||||
|
<div class="mediaplayer-volume-container visible-lg">
|
||||||
|
${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>
|
||||||
|
${shuffle}${repeat}
|
||||||
|
</message>
|
||||||
|
|
||||||
</messages>
|
</messages>
|
||||||
|
|||||||
+104
-23
@@ -1,5 +1,6 @@
|
|||||||
.main-nav {
|
.main-nav {
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
|
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.Wt-hrh2 {
|
.Wt-hrh2 {
|
||||||
@@ -10,13 +11,10 @@
|
|||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.contents {
|
|
||||||
padding: 0px 12px 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playqueue {
|
.playqueue {
|
||||||
background-color: #EEE;
|
background-color: #EEE;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.playqueue-playing {
|
.playqueue-playing {
|
||||||
@@ -39,32 +37,98 @@ div.contents {
|
|||||||
height: 64px;
|
height: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mediaplayer {
|
.mediaplayer-track {
|
||||||
background-color: #CCC;
|
|
||||||
border-radius: 10px;
|
|
||||||
min-width: 360px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mediaplayer-btn-controls {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mediaplayer-current-cover {
|
|
||||||
width: 72px;
|
|
||||||
height: 72px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mediaplayer-current-track {
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 120%;
|
font-size: 120%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mediaplayer-current-artist {
|
.mediaplayer-artist {
|
||||||
font-style: italic;
|
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 {
|
||||||
|
cursor: pointer;
|
||||||
|
text-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mediaplayer-btn-active {
|
||||||
|
color: #428bca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mediaplayer-info-container {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 100px;
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mediaplayer-track-info {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mediaplayer-progress-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mediaplayer-badge {
|
||||||
|
min-width: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mediaplayer-current-duration {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.mediaplayer-total-duration {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mediaplayer-seekbar {
|
||||||
|
}
|
||||||
|
|
||||||
|
.mediaplayer-volume-container {
|
||||||
|
width: 24px;
|
||||||
|
height: 64px;
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mediaplayer-volume {
|
||||||
|
height: 64px;
|
||||||
|
-webkit-appearance: slider-vertical;
|
||||||
|
writing-mode: bt-lr;
|
||||||
|
}
|
||||||
|
|
||||||
.mobile-search-title {
|
.mobile-search-title {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
@@ -117,3 +181,20 @@ div.contents {
|
|||||||
height: 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;
|
||||||
|
}
|
||||||
|
|||||||
Executable → Regular
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
+7
-6
@@ -20,13 +20,12 @@ lms_SOURCES = \
|
|||||||
$(srcdir)/image/Image.cpp \
|
$(srcdir)/image/Image.cpp \
|
||||||
$(srcdir)/logger/Logger.cpp \
|
$(srcdir)/logger/Logger.cpp \
|
||||||
$(srcdir)/metadata/AvFormat.cpp \
|
$(srcdir)/metadata/AvFormat.cpp \
|
||||||
$(srcdir)/metadata/Utils.cpp \
|
|
||||||
$(srcdir)/service/ServiceManager.cpp \
|
$(srcdir)/service/ServiceManager.cpp \
|
||||||
$(srcdir)/service/DatabaseUpdateService.cpp \
|
$(srcdir)/service/DatabaseUpdateService.cpp \
|
||||||
$(srcdir)/ui/LmsApplication.cpp \
|
$(srcdir)/ui/LmsApplication.cpp \
|
||||||
$(srcdir)/ui/auth/LmsAuth.cpp \
|
$(srcdir)/ui/auth/LmsAuth.cpp \
|
||||||
|
$(srcdir)/ui/audio/AudioPlayer.cpp \
|
||||||
$(srcdir)/ui/audio/desktop/DesktopAudio.cpp \
|
$(srcdir)/ui/audio/desktop/DesktopAudio.cpp \
|
||||||
$(srcdir)/ui/audio/desktop/AudioMediaPlayer.cpp \
|
|
||||||
$(srcdir)/ui/audio/desktop/FilterChain.cpp \
|
$(srcdir)/ui/audio/desktop/FilterChain.cpp \
|
||||||
$(srcdir)/ui/audio/desktop/KeywordSearchFilter.cpp \
|
$(srcdir)/ui/audio/desktop/KeywordSearchFilter.cpp \
|
||||||
$(srcdir)/ui/audio/desktop/PlayQueue.cpp \
|
$(srcdir)/ui/audio/desktop/PlayQueue.cpp \
|
||||||
@@ -34,13 +33,15 @@ lms_SOURCES = \
|
|||||||
$(srcdir)/ui/audio/desktop/TrackView.cpp \
|
$(srcdir)/ui/audio/desktop/TrackView.cpp \
|
||||||
$(srcdir)/ui/audio/mobile/ArtistSearch.cpp \
|
$(srcdir)/ui/audio/mobile/ArtistSearch.cpp \
|
||||||
$(srcdir)/ui/audio/mobile/MobileAudio.cpp \
|
$(srcdir)/ui/audio/mobile/MobileAudio.cpp \
|
||||||
$(srcdir)/ui/audio/mobile/MobileAudioMediaPlayer.cpp \
|
|
||||||
$(srcdir)/ui/audio/mobile/ReleaseSearch.cpp \
|
$(srcdir)/ui/audio/mobile/ReleaseSearch.cpp \
|
||||||
|
$(srcdir)/ui/audio/mobile/TrackReleaseView.cpp \
|
||||||
$(srcdir)/ui/audio/mobile/TrackSearch.cpp \
|
$(srcdir)/ui/audio/mobile/TrackSearch.cpp \
|
||||||
$(srcdir)/ui/common/DirectoryValidator.cpp \
|
$(srcdir)/ui/common/DirectoryValidator.cpp \
|
||||||
|
$(srcdir)/ui/common/InputRange.cpp \
|
||||||
$(srcdir)/ui/common/LineEdit.cpp \
|
$(srcdir)/ui/common/LineEdit.cpp \
|
||||||
$(srcdir)/ui/resource/AvConvTranscodeStreamResource.cpp \
|
$(srcdir)/ui/resource/AvConvTranscodeStreamResource.cpp \
|
||||||
$(srcdir)/ui/resource/CoverResource.cpp \
|
$(srcdir)/ui/resource/CoverResource.cpp \
|
||||||
|
$(srcdir)/ui/resource/TranscodeResource.cpp \
|
||||||
$(srcdir)/ui/settings/Settings.cpp \
|
$(srcdir)/ui/settings/Settings.cpp \
|
||||||
$(srcdir)/ui/settings/SettingsAccountFormView.cpp \
|
$(srcdir)/ui/settings/SettingsAccountFormView.cpp \
|
||||||
$(srcdir)/ui/settings/SettingsAudioFormView.cpp \
|
$(srcdir)/ui/settings/SettingsAudioFormView.cpp \
|
||||||
@@ -48,8 +49,9 @@ lms_SOURCES = \
|
|||||||
$(srcdir)/ui/settings/SettingsFirstConnectionFormView.cpp \
|
$(srcdir)/ui/settings/SettingsFirstConnectionFormView.cpp \
|
||||||
$(srcdir)/ui/settings/SettingsMediaDirectories.cpp \
|
$(srcdir)/ui/settings/SettingsMediaDirectories.cpp \
|
||||||
$(srcdir)/ui/settings/SettingsMediaDirectoryFormView.cpp \
|
$(srcdir)/ui/settings/SettingsMediaDirectoryFormView.cpp \
|
||||||
$(srcdir)/ui/settings/SettingsUserFormView.cpp \
|
$(srcdir)/ui/settings/SettingsUserFormView.cpp \
|
||||||
$(srcdir)/ui/settings/SettingsUsers.cpp
|
$(srcdir)/ui/settings/SettingsUsers.cpp \
|
||||||
|
$(srcdir)/utils/Utils.cpp
|
||||||
|
|
||||||
if VIDEO
|
if VIDEO
|
||||||
lms_SOURCES += \
|
lms_SOURCES += \
|
||||||
@@ -60,5 +62,4 @@ lms_SOURCES += \
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
lms_CXXFLAGS=-std=c++11 -Wall -I$(top_srcdir)/third-party -I$(srcdir)/ui $(MAGICKXX_CFLAGS)
|
lms_CXXFLAGS=-std=c++11 -Wall -I$(top_srcdir)/third-party -I$(srcdir)/ui $(MAGICKXX_CFLAGS)
|
||||||
|
|
||||||
lms_LDADD=$(MAGICKXX_LIBS)
|
lms_LDADD=$(MAGICKXX_LIBS)
|
||||||
|
|||||||
+65
-33
@@ -29,22 +29,55 @@ namespace Av {
|
|||||||
|
|
||||||
#define LMS_LOG_TRANSCODE(sev) LMS_LOG(TRANSCODE, INFO) << "[" << _id << "] - "
|
#define LMS_LOG_TRANSCODE(sev) LMS_LOG(TRANSCODE, INFO) << "[" << _id << "] - "
|
||||||
|
|
||||||
|
struct EncodingInfo
|
||||||
|
{
|
||||||
|
Encoding encoding;
|
||||||
|
std::string mimetype;
|
||||||
|
int id;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 encoding_to_mimetype(Encoding encoding)
|
||||||
{
|
{
|
||||||
switch(encoding)
|
for (auto encodingInfo : encodingInfos)
|
||||||
{
|
{
|
||||||
case Encoding::MP3: return "audio/mp3";
|
if (encodingInfo.encoding == encoding)
|
||||||
case Encoding::OGA: return "audio/ogg";
|
return encodingInfo.mimetype;
|
||||||
case Encoding::OGV: return "video/ogg";
|
|
||||||
case Encoding::WEBMA: return "audio/webm";
|
|
||||||
case Encoding::WEBMV: return "video/webm";
|
|
||||||
case Encoding::FLA: return "audio/x-flv";
|
|
||||||
case Encoding::FLV: return "video/x-flv";
|
|
||||||
case Encoding::M4A: return "audio/mp4";
|
|
||||||
case Encoding::M4V: return "video/mp4";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
throw std::logic_error("encoding_to_mimetype failed!");
|
||||||
|
}
|
||||||
|
|
||||||
|
int encoding_to_int(Encoding encoding)
|
||||||
|
{
|
||||||
|
for (auto encodingInfo : encodingInfos)
|
||||||
|
{
|
||||||
|
if (encodingInfo.encoding == encoding)
|
||||||
|
return encodingInfo.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw std::logic_error("encoding_to_int failed!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Encoding encoding_from_int(int encodingId)
|
||||||
|
{
|
||||||
|
for (auto encodingInfo : encodingInfos)
|
||||||
|
{
|
||||||
|
if (encodingInfo.id == encodingId)
|
||||||
|
return encodingInfo.encoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw std::logic_error("encoding_from_int failed!");
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO, parametrize?
|
// TODO, parametrize?
|
||||||
@@ -129,6 +162,7 @@ Transcoder::start()
|
|||||||
// in order not to block the whole forked process
|
// in order not to block the whole forked process
|
||||||
args.push_back("-loglevel");
|
args.push_back("-loglevel");
|
||||||
args.push_back("quiet");
|
args.push_back("quiet");
|
||||||
|
args.push_back("-nostdin");
|
||||||
|
|
||||||
// input Offset
|
// input Offset
|
||||||
if (_parameters.getOffset().total_seconds() > 0)
|
if (_parameters.getOffset().total_seconds() > 0)
|
||||||
@@ -230,25 +264,6 @@ Transcoder::start()
|
|||||||
args.push_back("m4v");
|
args.push_back("m4v");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Encoding::FLV:
|
|
||||||
args.push_back("-acodec");
|
|
||||||
args.push_back("libmp3lame");
|
|
||||||
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("flv");
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Encoding::FLA:
|
|
||||||
args.push_back("-acodec");
|
|
||||||
args.push_back("libmp3lame");
|
|
||||||
args.push_back("-f");
|
|
||||||
args.push_back("flv");
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -265,8 +280,8 @@ Transcoder::start()
|
|||||||
|
|
||||||
_child = std::make_shared<redi::ipstream>();
|
_child = std::make_shared<redi::ipstream>();
|
||||||
|
|
||||||
const redi::pstreams::pmode mode = redi::pstreams::pstdout; // | redi::pstreams::pstderr;
|
// Caution: stdin must have been closed before
|
||||||
_child->open(avConvPath.string(), args, mode);
|
_child->open(avConvPath.string(), args);
|
||||||
if (!_child->is_open())
|
if (!_child->is_open())
|
||||||
{
|
{
|
||||||
LMS_LOG_TRANSCODE(DEBUG) << "Exec failed!";
|
LMS_LOG_TRANSCODE(DEBUG) << "Exec failed!";
|
||||||
@@ -279,8 +294,8 @@ Transcoder::start()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
LMS_LOG_TRANSCODE(DEBUG) << "Stream opened!";
|
|
||||||
}
|
}
|
||||||
|
LMS_LOG_TRANSCODE(DEBUG) << "Stream opened!";
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -291,14 +306,31 @@ Transcoder::process(std::vector<unsigned char>& output, std::size_t maxSize)
|
|||||||
if (!_child || _isComplete)
|
if (!_child || _isComplete)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (_child->out().fail())
|
||||||
|
{
|
||||||
|
LMS_LOG_TRANSCODE(DEBUG) << "Stdout FAILED 2";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_child->out().eof())
|
||||||
|
{
|
||||||
|
LMS_LOG_TRANSCODE(DEBUG) << "Stdout ENDED 2";
|
||||||
|
}
|
||||||
|
|
||||||
output.resize(maxSize);
|
output.resize(maxSize);
|
||||||
|
|
||||||
|
LMS_LOG_TRANSCODE(DEBUG) << "Reading up to " << output.size() << " bytes";
|
||||||
|
|
||||||
//Read on the output stream
|
//Read on the output stream
|
||||||
_child->out().read(reinterpret_cast<char*>(&output[0]), maxSize);
|
_child->out().read(reinterpret_cast<char*>(&output[0]), maxSize);
|
||||||
output.resize(_child->out().gcount());
|
output.resize(_child->out().gcount());
|
||||||
|
|
||||||
LMS_LOG_TRANSCODE(DEBUG) << "Read " << output.size() << " bytes";
|
LMS_LOG_TRANSCODE(DEBUG) << "Read " << output.size() << " bytes";
|
||||||
|
|
||||||
|
if (_child->out().fail())
|
||||||
|
{
|
||||||
|
LMS_LOG_TRANSCODE(DEBUG) << "Stdout FAILED";
|
||||||
|
}
|
||||||
|
|
||||||
if (_child->out().eof())
|
if (_child->out().eof())
|
||||||
{
|
{
|
||||||
LMS_LOG_TRANSCODE(DEBUG) << "Stdout EOF!";
|
LMS_LOG_TRANSCODE(DEBUG) << "Stdout EOF!";
|
||||||
|
|||||||
@@ -40,13 +40,13 @@ enum class Encoding
|
|||||||
MP3,
|
MP3,
|
||||||
WEBMA,
|
WEBMA,
|
||||||
WEBMV,
|
WEBMV,
|
||||||
FLA,
|
|
||||||
FLV,
|
|
||||||
M4A,
|
M4A,
|
||||||
M4V,
|
M4V,
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string encoding_to_mimetype(Encoding encoding);
|
std::string encoding_to_mimetype(Encoding encoding);
|
||||||
|
int encoding_to_int(Encoding encoding);
|
||||||
|
Encoding encoding_from_int(int encoding);
|
||||||
|
|
||||||
class TranscodeParameters
|
class TranscodeParameters
|
||||||
{
|
{
|
||||||
@@ -91,6 +91,8 @@ class Transcoder
|
|||||||
void process(std::vector<unsigned char>& output, std::size_t maxSize);
|
void process(std::vector<unsigned char>& output, std::size_t maxSize);
|
||||||
bool isComplete(void) { return _isComplete; }
|
bool isComplete(void) { return _isComplete; }
|
||||||
|
|
||||||
|
const TranscodeParameters& getParameters() const { return _parameters; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Transcoder();
|
Transcoder();
|
||||||
|
|
||||||
|
|||||||
@@ -170,23 +170,25 @@ Grabber::getFromRelease(Wt::Dbo::Session& session, Database::Release::id_type re
|
|||||||
{
|
{
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
|
|
||||||
boost::filesystem::path firstTrackPath;
|
Wt::Dbo::Transaction transaction(session);
|
||||||
bool embeddedCover = false;
|
|
||||||
|
// If the release does not exist or is the special release "None", do nothing
|
||||||
|
Release::pointer release = Release::getById(session, releaseId);
|
||||||
|
if (!release || release->isNone())
|
||||||
|
return std::vector<Image::Image>();
|
||||||
|
|
||||||
// Get the first track of the release
|
// Get the first track of the release
|
||||||
{
|
std::vector<Track::pointer> tracks = Track::getByFilter(session,
|
||||||
Wt::Dbo::Transaction transaction(session);
|
SearchFilter::ById(SearchFilter::Field::Release, releaseId),
|
||||||
|
-1, 1 /* limit result size */);
|
||||||
|
|
||||||
std::vector<Track::pointer> tracks = Track::getByFilter(session,
|
if (tracks.empty())
|
||||||
SearchFilter::IdMatch({{SearchFilter::Field::Release, {releaseId}}}),
|
return std::vector<Image::Image>();
|
||||||
-1, 1 /* limit result size */);
|
|
||||||
|
|
||||||
if (tracks.empty())
|
boost::filesystem::path firstTrackPath = tracks.front()->getPath();
|
||||||
return std::vector<Image::Image>();
|
bool embeddedCover = (tracks.front()->getCoverType() == Track::CoverType::Embedded);
|
||||||
|
|
||||||
firstTrackPath = tracks.front()->getPath();
|
transaction.commit();
|
||||||
embeddedCover = (tracks.front()->getCoverType() == Track::CoverType::Embedded);
|
|
||||||
}
|
|
||||||
|
|
||||||
// First, try to get covers from the directory of the release
|
// First, try to get covers from the directory of the release
|
||||||
std::vector<Image::Image> res = getFromDirectory( firstTrackPath.parent_path(), nbMaxCovers);
|
std::vector<Image::Image> res = getFromDirectory( firstTrackPath.parent_path(), nbMaxCovers);
|
||||||
|
|||||||
@@ -526,9 +526,15 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats)
|
|||||||
if (items.find(MetaData::Type::TrackNumber) != items.end())
|
if (items.find(MetaData::Type::TrackNumber) != items.end())
|
||||||
track.modify()->setTrackNumber( boost::any_cast<std::size_t>(items[MetaData::Type::TrackNumber]) );
|
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())
|
if (items.find(MetaData::Type::DiscNumber) != items.end())
|
||||||
track.modify()->setDiscNumber( boost::any_cast<std::size_t>(items[MetaData::Type::DiscNumber]) );
|
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())
|
if (items.find(MetaData::Type::Date) != items.end())
|
||||||
track.modify()->setDate( boost::any_cast<boost::posix_time::ptime>(items[MetaData::Type::Date]) );
|
track.modify()->setDate( boost::any_cast<boost::posix_time::ptime>(items[MetaData::Type::Date]) );
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ Release::getByMBID(Wt::Dbo::Session& session, const std::string& mbid)
|
|||||||
return session.find<Release>().where("mbid = ?").bind(mbid);
|
return session.find<Release>().where("mbid = ?").bind(mbid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Release::pointer
|
||||||
|
Release::getById(Wt::Dbo::Session& session, Release::id_type id)
|
||||||
|
{
|
||||||
|
return session.find<Release>().where("id = ?").bind(id);
|
||||||
|
}
|
||||||
|
|
||||||
Release::pointer
|
Release::pointer
|
||||||
Release::create(Wt::Dbo::Session& session, const std::string& name, const std::string& MBID)
|
Release::create(Wt::Dbo::Session& session, const std::string& name, const std::string& MBID)
|
||||||
@@ -51,6 +56,12 @@ Release::create(Wt::Dbo::Session& session, const std::string& name, const std::s
|
|||||||
return session.add(new Release(name, MBID));
|
return session.add(new Release(name, MBID));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
Release::isNone() const
|
||||||
|
{
|
||||||
|
return _name == "<None>";
|
||||||
|
}
|
||||||
|
|
||||||
Release::pointer
|
Release::pointer
|
||||||
Release::getNone(Wt::Dbo::Session& session)
|
Release::getNone(Wt::Dbo::Session& session)
|
||||||
{
|
{
|
||||||
@@ -129,16 +140,28 @@ Release::getByFilter(Wt::Dbo::Session& session, SearchFilter filter, int offset,
|
|||||||
return std::vector<pointer>(res.begin(), res.end());
|
return std::vector<pointer>(res.begin(), res.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector< Wt::Dbo::ptr<Artist> >
|
int
|
||||||
Release::getArtists() const
|
Release::getReleaseYear(bool original) const
|
||||||
{
|
{
|
||||||
assert(self());
|
|
||||||
assert(self()->id() != Wt::Dbo::dbo_traits<Release>::invalidId() );
|
|
||||||
assert(session());
|
assert(session());
|
||||||
|
|
||||||
Wt::Dbo::collection< Wt::Dbo::ptr<Artist> > res = session()->query<Wt::Dbo::ptr<Artist> >("SELECT a FROM artist a INNER JOIN release r ON r.id = t.release_id INNER JOIN track t ON t.release_id = r.id").where("r.id = ?").bind(id());
|
// TODO something better
|
||||||
|
auto tracks = Track::getByFilter(*session(), SearchFilter::ById(SearchFilter::Field::Release, this->id()), -1, 1);
|
||||||
|
|
||||||
return std::vector< Wt::Dbo::ptr<Artist> > (res.begin(), res.end());
|
if (tracks.empty())
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
boost::gregorian::date date;
|
||||||
|
|
||||||
|
if (original)
|
||||||
|
date = tracks.front()->getOriginalDate().date();
|
||||||
|
else
|
||||||
|
date = tracks.front()->getDate().date();
|
||||||
|
|
||||||
|
if (date.is_special())
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return date.year();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Database
|
} // namespace Database
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ class Release : public Wt::Dbo::Dbo<Release>
|
|||||||
// Create
|
// Create
|
||||||
static pointer create(Wt::Dbo::Session& session, const std::string& name, const std::string& MBID = "");
|
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
|
||||||
|
|
||||||
// MVC models for the user interface
|
// MVC models for the user interface
|
||||||
// ID, Release name, year, track counts
|
// ID, Release name, year, track counts
|
||||||
typedef boost::tuple<id_type, std::string, boost::posix_time::ptime, int> UIQueryResult;
|
typedef boost::tuple<id_type, std::string, boost::posix_time::ptime, int> UIQueryResult;
|
||||||
@@ -65,8 +68,6 @@ class Release : public Wt::Dbo::Dbo<Release>
|
|||||||
std::string getMBID() const { return _MBID; }
|
std::string getMBID() const { return _MBID; }
|
||||||
bool isNone(void) const;
|
bool isNone(void) const;
|
||||||
boost::posix_time::time_duration getDuration(void) const;
|
boost::posix_time::time_duration getDuration(void) const;
|
||||||
std::vector<Wt::Dbo::ptr<Artist> > getArtists() const; // Get the artists of this release
|
|
||||||
std::vector<Wt::Dbo::ptr<Track> > getTracks() const; // Get the tracks of this release
|
|
||||||
|
|
||||||
void setMBID(std::string mbid) { _MBID = mbid; }
|
void setMBID(std::string mbid) { _MBID = mbid; }
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include "logger/Logger.hpp"
|
||||||
|
|
||||||
#include "SearchFilter.hpp"
|
#include "SearchFilter.hpp"
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
|
|||||||
@@ -58,12 +58,29 @@ class SearchFilter
|
|||||||
return SearchFilter(_nameLikeMatch);
|
return SearchFilter(_nameLikeMatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single ID match
|
// Single Field ID match
|
||||||
static SearchFilter ById(Field field, Wt::Dbo::dbo_default_traits::IdType id)
|
static SearchFilter ById(Field field, Wt::Dbo::dbo_default_traits::IdType id)
|
||||||
{
|
{
|
||||||
return SearchFilter({{field, {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:
|
// 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 ...) ...
|
// ((Field1.name LIKE STR1-1 OR Field1.name LIKE STR1-2 ...) OR (Field2.name LIKE STR2-1 OR Field2.name LIKE STR2-2 ...) ...
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ namespace Database {
|
|||||||
Track::Track(const boost::filesystem::path& p)
|
Track::Track(const boost::filesystem::path& p)
|
||||||
:
|
:
|
||||||
_trackNumber(0),
|
_trackNumber(0),
|
||||||
|
_totalTrackNumber(0),
|
||||||
_discNumber(0),
|
_discNumber(0),
|
||||||
|
_totalDiscNumber(0),
|
||||||
_filePath( p.string() ),
|
_filePath( p.string() ),
|
||||||
_coverType(CoverType::None)
|
_coverType(CoverType::None)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -136,7 +136,9 @@ class Track
|
|||||||
|
|
||||||
// Accessors
|
// Accessors
|
||||||
void setTrackNumber(int num) { _trackNumber = num; }
|
void setTrackNumber(int num) { _trackNumber = num; }
|
||||||
|
void setTotalTrackNumber(int num) { _totalTrackNumber = num; }
|
||||||
void setDiscNumber(int num) { _discNumber = 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 setName(const std::string& name) { _name = std::string(name, 0, _maxNameLength); }
|
||||||
void setDuration(boost::posix_time::time_duration duration) { _duration = duration; }
|
void setDuration(boost::posix_time::time_duration duration) { _duration = duration; }
|
||||||
void setLastWriteTime(boost::posix_time::ptime time) { _fileLastWrite = time; }
|
void setLastWriteTime(boost::posix_time::ptime time) { _fileLastWrite = time; }
|
||||||
@@ -146,13 +148,15 @@ class Track
|
|||||||
void setOriginalDate(const boost::posix_time::ptime& date) { _originalDate = date; }
|
void setOriginalDate(const boost::posix_time::ptime& date) { _originalDate = date; }
|
||||||
void setGenres(const std::string& genreList) { _genreList = genreList; }
|
void setGenres(const std::string& genreList) { _genreList = genreList; }
|
||||||
void setCoverType(CoverType coverType) { _coverType = coverType; }
|
void setCoverType(CoverType coverType) { _coverType = coverType; }
|
||||||
void setMBID(const std::string& MBID) { _MBID = MBID; }
|
void setMBID(const std::string& MBID) { _MBID = MBID; }
|
||||||
void setArtist(Wt::Dbo::ptr<Artist> artist) { _artist = artist; }
|
void setArtist(Wt::Dbo::ptr<Artist> artist) { _artist = artist; }
|
||||||
void setRelease(Wt::Dbo::ptr<Release> release) { _release = release; }
|
void setRelease(Wt::Dbo::ptr<Release> release) { _release = release; }
|
||||||
void setGenres(std::vector<Genre::pointer> genres);
|
void setGenres(std::vector<Genre::pointer> genres);
|
||||||
|
|
||||||
int getTrackNumber(void) const { return _trackNumber; }
|
int getTrackNumber(void) const { return _trackNumber; }
|
||||||
|
int getTotalTrackNumber(void) const { return _totalTrackNumber; }
|
||||||
int getDiscNumber(void) const { return _discNumber; }
|
int getDiscNumber(void) const { return _discNumber; }
|
||||||
|
int getTotalDiscNumber(void) const { return _totalDiscNumber; }
|
||||||
std::string getName(void) const { return _name; }
|
std::string getName(void) const { return _name; }
|
||||||
boost::filesystem::path getPath(void) const { return _filePath; }
|
boost::filesystem::path getPath(void) const { return _filePath; }
|
||||||
boost::posix_time::time_duration getDuration(void) const { return _duration; }
|
boost::posix_time::time_duration getDuration(void) const { return _duration; }
|
||||||
@@ -172,7 +176,9 @@ class Track
|
|||||||
void persist(Action& a)
|
void persist(Action& a)
|
||||||
{
|
{
|
||||||
Wt::Dbo::field(a, _trackNumber, "track_number");
|
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, _discNumber, "disc_number");
|
||||||
|
Wt::Dbo::field(a, _totalDiscNumber, "total_disc_number");
|
||||||
Wt::Dbo::field(a, _name, "name");
|
Wt::Dbo::field(a, _name, "name");
|
||||||
Wt::Dbo::field(a, _duration, "duration");
|
Wt::Dbo::field(a, _duration, "duration");
|
||||||
Wt::Dbo::field(a, _date, "date");
|
Wt::Dbo::field(a, _date, "date");
|
||||||
@@ -197,7 +203,9 @@ class Track
|
|||||||
static const std::size_t _maxNameLength = 128;
|
static const std::size_t _maxNameLength = 128;
|
||||||
|
|
||||||
int _trackNumber;
|
int _trackNumber;
|
||||||
|
int _totalTrackNumber;
|
||||||
int _discNumber;
|
int _discNumber;
|
||||||
|
int _totalDiscNumber;
|
||||||
std::string _name;
|
std::string _name;
|
||||||
std::string _artistName;
|
std::string _artistName;
|
||||||
std::string _releaseName;
|
std::string _releaseName;
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ User::audioEncodings =
|
|||||||
AudioEncoding::MP3,
|
AudioEncoding::MP3,
|
||||||
AudioEncoding::OGA,
|
AudioEncoding::OGA,
|
||||||
AudioEncoding::WEBMA,
|
AudioEncoding::WEBMA,
|
||||||
AudioEncoding::FLA,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const std::vector<VideoEncoding>
|
const std::vector<VideoEncoding>
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ enum class AudioEncoding
|
|||||||
MP3,
|
MP3,
|
||||||
OGA,
|
OGA,
|
||||||
WEBMA,
|
WEBMA,
|
||||||
FLA,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class VideoEncoding
|
enum class VideoEncoding
|
||||||
|
|||||||
@@ -17,8 +17,7 @@
|
|||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef LOGGER_HPP__
|
#pragma once
|
||||||
#define LOGGER_HPP__
|
|
||||||
|
|
||||||
#include <Wt/WServer>
|
#include <Wt/WServer>
|
||||||
#include <Wt/WApplication>
|
#include <Wt/WApplication>
|
||||||
@@ -55,4 +54,3 @@ std::string getSeverityName(Severity sev);
|
|||||||
|
|
||||||
#define LMS_LOG(module, level) Wt::log(getSeverityName(Severity::level)) << Wt::WLogger::sep << "[" << getModuleName(Module::module) << "]" << Wt::WLogger::sep
|
#define LMS_LOG(module, level) Wt::log(getSeverityName(Severity::level)) << Wt::WLogger::sep << "[" << getModuleName(Module::module) << "]" << Wt::WLogger::sep
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ int main(int argc, char* argv[])
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Make pstream work with ffmpeg
|
||||||
|
close(STDIN_FILENO);
|
||||||
|
|
||||||
Wt::WServer server(argv[0]);
|
Wt::WServer server(argv[0]);
|
||||||
server.setServerConfiguration (argc, argv);
|
server.setServerConfiguration (argc, argv);
|
||||||
|
|||||||
+46
-19
@@ -19,15 +19,12 @@
|
|||||||
|
|
||||||
#include "AvFormat.hpp"
|
#include "AvFormat.hpp"
|
||||||
|
|
||||||
#include <boost/foreach.hpp>
|
#include "logger/Logger.hpp"
|
||||||
|
#include "utils/Utils.hpp"
|
||||||
#include <boost/algorithm/string.hpp>
|
#include <boost/algorithm/string.hpp>
|
||||||
#include <boost/date_time/posix_time/posix_time.hpp>
|
|
||||||
|
|
||||||
#include "av/AvInfo.hpp"
|
#include "av/AvInfo.hpp"
|
||||||
|
|
||||||
#include "logger/Logger.hpp"
|
|
||||||
|
|
||||||
#include "Utils.hpp"
|
|
||||||
|
|
||||||
namespace MetaData
|
namespace MetaData
|
||||||
{
|
{
|
||||||
@@ -112,21 +109,48 @@ AvFormat::parse(const boost::filesystem::path& p, Items& items)
|
|||||||
for (it = metadata.begin(); it != metadata.end(); ++it)
|
for (it = metadata.begin(); it != metadata.end(); ++it)
|
||||||
{
|
{
|
||||||
if (boost::iequals(it->first, "artist"))
|
if (boost::iequals(it->first, "artist"))
|
||||||
items.insert( std::make_pair(MetaData::Type::Artist, string_trim( string_to_utf8(it->second)) ));
|
items.insert( std::make_pair(MetaData::Type::Artist, stringTrim( stringToUTF8(it->second)) ));
|
||||||
else if (boost::iequals(it->first, "album"))
|
else if (boost::iequals(it->first, "album"))
|
||||||
items.insert( std::make_pair(MetaData::Type::Album, string_trim( string_to_utf8(it->second)) ));
|
items.insert( std::make_pair(MetaData::Type::Album, stringTrim( stringToUTF8(it->second)) ));
|
||||||
else if (boost::iequals(it->first, "title"))
|
else if (boost::iequals(it->first, "title"))
|
||||||
items.insert( std::make_pair(MetaData::Type::Title, string_trim( string_to_utf8(it->second)) ));
|
items.insert( std::make_pair(MetaData::Type::Title, stringTrim( stringToUTF8(it->second)) ));
|
||||||
else if (boost::iequals(it->first, "track")) {
|
else if (boost::iequals(it->first, "track"))
|
||||||
std::size_t number;
|
{
|
||||||
if (readAs<std::size_t>(it->second, number))
|
// Expecting 'Number/Total'
|
||||||
items.insert( std::make_pair(MetaData::Type::TrackNumber, number ));
|
auto strings = splitString(it->second, "/");
|
||||||
|
|
||||||
|
if (strings.size() > 0)
|
||||||
|
{
|
||||||
|
std::size_t number;
|
||||||
|
if (readAs<std::size_t>(strings[0], number))
|
||||||
|
items.insert( std::make_pair(MetaData::Type::TrackNumber, number ));
|
||||||
|
|
||||||
|
if (strings.size() > 1)
|
||||||
|
{
|
||||||
|
std::size_t totalNumber;
|
||||||
|
if (readAs<std::size_t>(strings[1], totalNumber))
|
||||||
|
items.insert( std::make_pair(MetaData::Type::TotalTrack, totalNumber ));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (boost::iequals(it->first, "disc"))
|
else if (boost::iequals(it->first, "disc"))
|
||||||
{
|
{
|
||||||
std::size_t number;
|
// Expecting 'Number/Total'
|
||||||
if (readAs<std::size_t>(it->second, number))
|
auto strings = splitString(it->second, "/");
|
||||||
items.insert( std::make_pair(MetaData::Type::DiscNumber, number ));
|
|
||||||
|
if (strings.size() > 0)
|
||||||
|
{
|
||||||
|
std::size_t number;
|
||||||
|
if (readAs<std::size_t>(strings[0], number))
|
||||||
|
items.insert( std::make_pair(MetaData::Type::DiscNumber, number ));
|
||||||
|
|
||||||
|
if (strings.size() > 1)
|
||||||
|
{
|
||||||
|
std::size_t totalNumber;
|
||||||
|
if (readAs<std::size_t>(strings[1], totalNumber))
|
||||||
|
items.insert( std::make_pair(MetaData::Type::TotalDisc, totalNumber ));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (boost::iequals(it->first, "date")
|
else if (boost::iequals(it->first, "date")
|
||||||
|| boost::iequals(it->first, "year")
|
|| boost::iequals(it->first, "year")
|
||||||
@@ -145,18 +169,21 @@ AvFormat::parse(const boost::filesystem::path& p, Items& items)
|
|||||||
}
|
}
|
||||||
else if (boost::iequals(it->first, "genre"))
|
else if (boost::iequals(it->first, "genre"))
|
||||||
{
|
{
|
||||||
|
// TODO use splitStrings
|
||||||
std::list<std::string> genres;
|
std::list<std::string> genres;
|
||||||
if (readList(it->second, ";,", genres))
|
if (readList(it->second, ";,", genres))
|
||||||
items.insert( std::make_pair(MetaData::Type::Genres, genres));
|
items.insert( std::make_pair(MetaData::Type::Genres, genres));
|
||||||
|
|
||||||
}
|
}
|
||||||
else if (boost::iequals(it->first, "MusicBrainz Artist Id"))
|
else if (boost::iequals(it->first, "MusicBrainz Artist Id")
|
||||||
|
|| boost::iequals(it->first, "MUSICBRAINZ_ARTISTID"))
|
||||||
{
|
{
|
||||||
items.insert( std::make_pair(MetaData::Type::MusicBrainzArtistID, string_trim( string_to_utf8(it->second)) ));
|
items.insert( std::make_pair(MetaData::Type::MusicBrainzArtistID, stringTrim( stringToUTF8(it->second)) ));
|
||||||
}
|
}
|
||||||
else if (boost::iequals(it->first, "MusicBrainz Album Id"))
|
else if (boost::iequals(it->first, "MusicBrainz Album Id")
|
||||||
|
|| boost::iequals(it->first, "MUSICBRAINZ_ALBUMID"))
|
||||||
{
|
{
|
||||||
items.insert( std::make_pair(MetaData::Type::MusicBrainzAlbumID, string_trim( string_to_utf8(it->second)) ));
|
items.insert( std::make_pair(MetaData::Type::MusicBrainzAlbumID, stringTrim( stringToUTF8(it->second)) ));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
namespace MetaData
|
namespace MetaData
|
||||||
{
|
{
|
||||||
|
|
||||||
// Implements AVFORMAT library
|
// Parse that makes use of AvFormat
|
||||||
class AvFormat : public Parser
|
class AvFormat : public Parser
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ namespace MetaData
|
|||||||
Duration, // boost::posix_time::time_duration
|
Duration, // boost::posix_time::time_duration
|
||||||
TrackNumber, // size_t
|
TrackNumber, // size_t
|
||||||
DiscNumber, // size_t
|
DiscNumber, // size_t
|
||||||
|
TotalTrack, // size_t
|
||||||
|
TotalDisc, // size_t
|
||||||
Date, // boost::posix_time::ptime
|
Date, // boost::posix_time::ptime
|
||||||
OriginalDate, // boost::posix_time::ptime
|
OriginalDate, // boost::posix_time::ptime
|
||||||
HasCover, // bool
|
HasCover, // bool
|
||||||
|
|||||||
+27
-19
@@ -23,7 +23,6 @@
|
|||||||
#include <Wt/WNavigationBar>
|
#include <Wt/WNavigationBar>
|
||||||
#include <Wt/WStackedWidget>
|
#include <Wt/WStackedWidget>
|
||||||
#include <Wt/WMenu>
|
#include <Wt/WMenu>
|
||||||
#include <Wt/WNavigationBar>
|
|
||||||
#include <Wt/WPopupMenu>
|
#include <Wt/WPopupMenu>
|
||||||
#include <Wt/WVBoxLayout>
|
#include <Wt/WVBoxLayout>
|
||||||
#include <Wt/Auth/Identity>
|
#include <Wt/Auth/Identity>
|
||||||
@@ -100,6 +99,7 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnecti
|
|||||||
setTheme(bootstrapTheme);
|
setTheme(bootstrapTheme);
|
||||||
|
|
||||||
useStyleSheet("css/lms.css");
|
useStyleSheet("css/lms.css");
|
||||||
|
useStyleSheet("resources/font-awesome/css/font-awesome.min.css");
|
||||||
|
|
||||||
// Add a resource bundle
|
// Add a resource bundle
|
||||||
messageResourceBundle().use(appRoot() + "templates");
|
messageResourceBundle().use(appRoot() + "templates");
|
||||||
@@ -142,6 +142,16 @@ Database::User::pointer CurrentUser()
|
|||||||
return DbHandler().getCurrentUser();
|
return DbHandler().getCurrentUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CoverResource* SessionCoverResource()
|
||||||
|
{
|
||||||
|
return LmsApplication::instance()->getCoverResource();
|
||||||
|
}
|
||||||
|
|
||||||
|
TranscodeResource* SessionTranscodeResource()
|
||||||
|
{
|
||||||
|
return LmsApplication::instance()->getTranscodeResource();
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
LmsApplication::createFirstConnectionUI()
|
LmsApplication::createFirstConnectionUI()
|
||||||
{
|
{
|
||||||
@@ -155,6 +165,8 @@ void
|
|||||||
LmsApplication::createLmsUI()
|
LmsApplication::createLmsUI()
|
||||||
{
|
{
|
||||||
_coverResource = new CoverResource(_db, root());
|
_coverResource = new CoverResource(_db, root());
|
||||||
|
_transcodeResource = new TranscodeResource(_db, root());
|
||||||
|
|
||||||
DbHandler().getLogin().changed().connect(this, &LmsApplication::handleAuthEvent);
|
DbHandler().getLogin().changed().connect(this, &LmsApplication::handleAuthEvent);
|
||||||
|
|
||||||
LmsAuth *authWidget = new LmsAuth();
|
LmsAuth *authWidget = new LmsAuth();
|
||||||
@@ -191,9 +203,15 @@ LmsApplication::handleAuthEvent(void)
|
|||||||
contentsStack->setOverflow(Wt::WContainerWidget::OverflowAuto);
|
contentsStack->setOverflow(Wt::WContainerWidget::OverflowAuto);
|
||||||
contentsStack->addStyleClass("contents");
|
contentsStack->addStyleClass("contents");
|
||||||
|
|
||||||
// Setup a Left-aligned menu.
|
Wt::WMenu *menu = new Wt::WMenu(contentsStack);
|
||||||
Wt::WMenu *leftMenu = new Wt::WMenu(contentsStack);
|
navigation->addMenu(menu, Wt::AlignRight);
|
||||||
navigation->addMenu(leftMenu);
|
|
||||||
|
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
|
||||||
|
|
||||||
Audio *audio;
|
Audio *audio;
|
||||||
|
|
||||||
@@ -202,23 +220,18 @@ LmsApplication::handleAuthEvent(void)
|
|||||||
else
|
else
|
||||||
audio = new Desktop::Audio();
|
audio = new Desktop::Audio();
|
||||||
|
|
||||||
leftMenu->addItem("Audio", audio);
|
menu->addItem("Audio", audio);
|
||||||
#if defined HAVE_VIDEO
|
#if defined HAVE_VIDEO
|
||||||
leftMenu->addItem("Video", new VideoWidget());
|
menu->addItem("Video", new VideoWidget());
|
||||||
#endif
|
#endif
|
||||||
leftMenu->addItem("Settings", new Settings::Settings());
|
menu->addItem("Settings", new Settings::Settings());
|
||||||
|
|
||||||
// Setup a Right-aligned menu.
|
|
||||||
Wt::WMenu *rightMenu = new Wt::WMenu();
|
|
||||||
|
|
||||||
navigation->addMenu(rightMenu, Wt::AlignRight);
|
|
||||||
|
|
||||||
Wt::WPopupMenu *popup = new Wt::WPopupMenu();
|
Wt::WPopupMenu *popup = new Wt::WPopupMenu();
|
||||||
popup->addItem("Logout");
|
popup->addItem("Logout");
|
||||||
|
|
||||||
Wt::WMenuItem *item = new Wt::WMenuItem( CurrentAuthUser().identity(Wt::Auth::Identity::LoginName) );
|
Wt::WMenuItem *item = new Wt::WMenuItem( CurrentAuthUser().identity(Wt::Auth::Identity::LoginName) );
|
||||||
item->setMenu(popup);
|
item->setMenu(popup);
|
||||||
rightMenu->addItem(item);
|
menu->addItem(item);
|
||||||
|
|
||||||
popup->itemSelected().connect(std::bind([=] (Wt::WMenuItem* item)
|
popup->itemSelected().connect(std::bind([=] (Wt::WMenuItem* item)
|
||||||
{
|
{
|
||||||
@@ -226,17 +239,12 @@ LmsApplication::handleAuthEvent(void)
|
|||||||
DbHandler().getLogin().logout();
|
DbHandler().getLogin().logout();
|
||||||
}, std::placeholders::_1));
|
}, std::placeholders::_1));
|
||||||
|
|
||||||
// Add a Search control.
|
|
||||||
LineEdit *searchEdit = new LineEdit(500);
|
|
||||||
searchEdit->setEmptyText("Search...");
|
|
||||||
|
|
||||||
searchEdit->timedChanged().connect(std::bind([=] ()
|
searchEdit->timedChanged().connect(std::bind([=] ()
|
||||||
{
|
{
|
||||||
// TODO: check which view is activated and search into it
|
// TODO: check which view is activated and search into it
|
||||||
audio->search(searchEdit->text().toUTF8());
|
audio->search(searchEdit->text().toUTF8());
|
||||||
}));
|
}));
|
||||||
|
|
||||||
navigation->addSearch(searchEdit, Wt::AlignLeft);
|
|
||||||
|
|
||||||
layout->addWidget(navigation);
|
layout->addWidget(navigation);
|
||||||
layout->addWidget(contentsStack, 1);
|
layout->addWidget(contentsStack, 1);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
|
|
||||||
#include "database/DatabaseHandler.hpp"
|
#include "database/DatabaseHandler.hpp"
|
||||||
#include "resource/CoverResource.hpp"
|
#include "resource/CoverResource.hpp"
|
||||||
|
#include "resource/TranscodeResource.hpp"
|
||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface {
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ class LmsApplication : public Wt::WApplication
|
|||||||
|
|
||||||
// Session application data
|
// Session application data
|
||||||
CoverResource* getCoverResource() { return _coverResource; }
|
CoverResource* getCoverResource() { return _coverResource; }
|
||||||
|
TranscodeResource* getTranscodeResource() { return _transcodeResource; }
|
||||||
Database::Handler& getDbHandler() { return _db;}
|
Database::Handler& getDbHandler() { return _db;}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -50,6 +52,7 @@ class LmsApplication : public Wt::WApplication
|
|||||||
|
|
||||||
Database::Handler _db;
|
Database::Handler _db;
|
||||||
CoverResource* _coverResource;
|
CoverResource* _coverResource;
|
||||||
|
TranscodeResource* _transcodeResource;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helpers to get session data
|
// Helpers to get session data
|
||||||
@@ -59,6 +62,9 @@ Wt::Dbo::Session& DboSession();
|
|||||||
const Wt::Auth::User& CurrentAuthUser();
|
const Wt::Auth::User& CurrentAuthUser();
|
||||||
Database::User::pointer CurrentUser();
|
Database::User::pointer CurrentUser();
|
||||||
|
|
||||||
|
CoverResource *SessionCoverResource();
|
||||||
|
TranscodeResource *SessionTranscodeResource();
|
||||||
|
|
||||||
} // namespace UserInterface
|
} // namespace UserInterface
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -0,0 +1,338 @@
|
|||||||
|
/*
|
||||||
|
* 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 <Wt/WText>
|
||||||
|
#include <Wt/WAudio>
|
||||||
|
#include <Wt/WImage>
|
||||||
|
#include <Wt/WTemplate>
|
||||||
|
#include <Wt/WPushButton>
|
||||||
|
|
||||||
|
#include "common/InputRange.hpp"
|
||||||
|
#include "logger/Logger.hpp"
|
||||||
|
#include "utils/Utils.hpp"
|
||||||
|
|
||||||
|
#include "LmsApplication.hpp"
|
||||||
|
|
||||||
|
#include "AudioPlayer.hpp"
|
||||||
|
|
||||||
|
namespace UserInterface {
|
||||||
|
|
||||||
|
|
||||||
|
Av::Encoding
|
||||||
|
AudioPlayer::getBestEncoding() const
|
||||||
|
{
|
||||||
|
// TODO get the supported formats of the player and pick one
|
||||||
|
return Av::Encoding::MP3;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
AudioPlayer::loadTrack(Database::Track::id_type trackId)
|
||||||
|
{
|
||||||
|
// TODO reduce this scope
|
||||||
|
Wt::Dbo::Transaction transaction(DboSession());
|
||||||
|
|
||||||
|
Database::Track::pointer track = Database::Track::getById(DboSession(), trackId);
|
||||||
|
if (!track)
|
||||||
|
{
|
||||||
|
LMS_LOG(UI, INFO) << "No track found for id " << trackId;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Database::User::pointer user = CurrentUser();
|
||||||
|
if (!user)
|
||||||
|
{
|
||||||
|
LMS_LOG(UI, ERROR) << "No user found!";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Av::Encoding encoding;
|
||||||
|
switch (user->getAudioEncoding())
|
||||||
|
{
|
||||||
|
case Database::AudioEncoding::MP3: encoding = Av::Encoding::MP3; break;
|
||||||
|
case Database::AudioEncoding::OGA: encoding = Av::Encoding::OGA; break;
|
||||||
|
case Database::AudioEncoding::WEBMA: encoding = Av::Encoding::WEBMA; break;
|
||||||
|
case Database::AudioEncoding::AUTO:
|
||||||
|
default:
|
||||||
|
encoding = getBestEncoding();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
bindString("track", Wt::WString::fromUTF8(track->getName()));
|
||||||
|
bindString("artist", Wt::WString::fromUTF8(track->getArtist()->getName()));
|
||||||
|
_cover->setImageLink(SessionCoverResource()->getTrackUrl(trackId, 64));
|
||||||
|
|
||||||
|
std::string durationFormat = track->getDuration().total_seconds() < 3600 ? "%M:%S" : "%H:%M:%S";
|
||||||
|
|
||||||
|
_trackDuration->setText(durationToString(track->getDuration(), durationFormat));
|
||||||
|
_trackCurTime->setText(durationToString(boost::posix_time::seconds(0), durationFormat));
|
||||||
|
|
||||||
|
// Analyse track, select the best media stream
|
||||||
|
Av::MediaFile mediaFile(track->getPath());
|
||||||
|
|
||||||
|
if (!mediaFile.open() || !mediaFile.scan())
|
||||||
|
{
|
||||||
|
LMS_LOG(UI, ERROR) << "Cannot open file '" << track->getPath();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int audioBestStreamId = mediaFile.getBestStreamId(Av::Stream::Type::Audio);
|
||||||
|
std::vector<std::size_t> streams;
|
||||||
|
if (audioBestStreamId != -1)
|
||||||
|
streams.push_back(audioBestStreamId);
|
||||||
|
|
||||||
|
this->doJavaScript("\
|
||||||
|
document.lms.audio.state = \"loaded\";\
|
||||||
|
document.lms.audio.seekbar.min = " + std::to_string(0) + ";\
|
||||||
|
document.lms.audio.seekbar.max = " + std::to_string(track->getDuration().total_seconds()) + ";\
|
||||||
|
document.lms.audio.seekbar.value = 0;\
|
||||||
|
document.lms.audio.seekbar.disabled = false;\
|
||||||
|
document.lms.audio.offset = 0;\
|
||||||
|
document.lms.audio.curTime = 0;\
|
||||||
|
");
|
||||||
|
|
||||||
|
LMS_LOG(UI, DEBUG) << "Loading, URL = '" << SessionTranscodeResource()->getUrl(trackId, encoding, 0, streams) << "'";
|
||||||
|
|
||||||
|
//TODO, try to load everything in JS in order to prevent the WriteError bug?
|
||||||
|
_audio->pause();
|
||||||
|
_audio->clearSources();
|
||||||
|
_audio->addSource(SessionTranscodeResource()->getUrl(trackId, encoding, 0, streams));
|
||||||
|
_audio->setPreloadMode(Wt::WAudio::PreloadNone);
|
||||||
|
_audio->play();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioPlayer::AudioPlayer(Wt::WContainerWidget *parent)
|
||||||
|
: Wt::WTemplate(parent)
|
||||||
|
{
|
||||||
|
setTemplateText(Wt::WString::tr("wa-audio-player"));
|
||||||
|
addStyleClass("mediaplayer");
|
||||||
|
|
||||||
|
// TODO potential leak here
|
||||||
|
_audio = new Wt::WAudio();
|
||||||
|
_audio->setOptions(Wt::WAudio::Autoplay);
|
||||||
|
_audio->setPreloadMode(Wt::WAudio::PreloadNone);
|
||||||
|
bindWidget("audio", _audio);
|
||||||
|
|
||||||
|
_audio->ended().connect(std::bind([=] ()
|
||||||
|
{
|
||||||
|
_playbackEnded.emit();
|
||||||
|
}));
|
||||||
|
|
||||||
|
_cover = new Wt::WImage();
|
||||||
|
bindWidget("cover", _cover);
|
||||||
|
_cover->setImageLink(SessionCoverResource()->getUnknownTrackUrl(64));
|
||||||
|
|
||||||
|
InputRange *seekbar = new InputRange();
|
||||||
|
seekbar->addStyleClass("mediaplayer-seekbar");
|
||||||
|
bindWidget("seekbar", seekbar);
|
||||||
|
|
||||||
|
bindString("track", "Track");
|
||||||
|
bindString("artist", "Artist");
|
||||||
|
|
||||||
|
InputRange *volumeSlider = new InputRange();
|
||||||
|
volumeSlider->addStyleClass("mediaplayer-volume");
|
||||||
|
volumeSlider->setAttributeValue("orient", "vertical"); // firefox
|
||||||
|
bindWidget("volume", volumeSlider);
|
||||||
|
|
||||||
|
Wt::WText *repeatBtn = new Wt::WText("<i class=\"fa fa-repeat fa-2x\"></i>", Wt::XHTMLText);
|
||||||
|
repeatBtn->addStyleClass("mediaplayer-btn");
|
||||||
|
bindWidget("repeat", repeatBtn);
|
||||||
|
repeatBtn->clicked().connect(std::bind([=] ()
|
||||||
|
{
|
||||||
|
if (repeatBtn->hasStyleClass("mediaplayer-btn-active"))
|
||||||
|
{
|
||||||
|
repeatBtn->removeStyleClass("mediaplayer-btn-active");
|
||||||
|
_loop.emit(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
repeatBtn->addStyleClass("mediaplayer-btn-active");
|
||||||
|
_loop.emit(true);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
Wt::WText *shuffleBtn = new Wt::WText("<i class=\"fa fa-random fa-2x\"></i>", Wt::XHTMLText);
|
||||||
|
shuffleBtn->addStyleClass("mediaplayer-btn");
|
||||||
|
bindWidget("shuffle", shuffleBtn);
|
||||||
|
shuffleBtn->clicked().connect(std::bind([=] ()
|
||||||
|
{
|
||||||
|
if (shuffleBtn->hasStyleClass("mediaplayer-btn-active"))
|
||||||
|
{
|
||||||
|
shuffleBtn->removeStyleClass("mediaplayer-btn-active");
|
||||||
|
_shuffle.emit(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
shuffleBtn->addStyleClass("mediaplayer-btn-active");
|
||||||
|
_shuffle.emit(true);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
Wt::WText *prevBtn = new Wt::WText("<i class=\"fa fa-step-backward fa-2x\"></i>", Wt::XHTMLText);
|
||||||
|
prevBtn->addStyleClass("mediaplayer-btn hidden-xs");
|
||||||
|
bindWidget("prev", prevBtn);
|
||||||
|
prevBtn->clicked().connect(std::bind([=] ()
|
||||||
|
{
|
||||||
|
_playPrevious.emit();
|
||||||
|
}));
|
||||||
|
|
||||||
|
Wt::WText *nextBtn = new Wt::WText("<i class=\"fa fa-step-forward fa-2x\"></i>", Wt::XHTMLText);
|
||||||
|
nextBtn->addStyleClass("mediaplayer-btn");
|
||||||
|
bindWidget("next", nextBtn);
|
||||||
|
nextBtn->clicked().connect(std::bind([=] ()
|
||||||
|
{
|
||||||
|
_playNext.emit();
|
||||||
|
}));
|
||||||
|
|
||||||
|
Wt::WText *playPauseBtn = new Wt::WText("<i class=\"fa fa-play fa-3x fa-fw\"></i>", Wt::XHTMLText);
|
||||||
|
playPauseBtn->addStyleClass("mediaplayer-btn");
|
||||||
|
bindWidget("play-pause", playPauseBtn);
|
||||||
|
|
||||||
|
_trackCurTime = new Wt::WText("--:--");
|
||||||
|
_trackCurTime->addStyleClass("hidden-xs badge mediaplayer-badge mediaplayer-current-duration");
|
||||||
|
bindWidget("curtime", _trackCurTime);
|
||||||
|
|
||||||
|
_trackDuration = new Wt::WText("--:--");
|
||||||
|
_trackDuration->addStyleClass("hidden-xs badge mediaplayer-badge mediaplayer-total-duration");
|
||||||
|
bindWidget("duration", _trackDuration);
|
||||||
|
|
||||||
|
this->doJavaScript(
|
||||||
|
"\
|
||||||
|
document.lms = {};\
|
||||||
|
document.lms.audio = {};\
|
||||||
|
document.lms.audio.audio = " + _audio->jsRef() + ";\
|
||||||
|
document.lms.audio.seekbar = " + seekbar->jsRef() +";\
|
||||||
|
document.lms.audio.volumeSlider = " + volumeSlider->jsRef() + ";\
|
||||||
|
document.lms.audio.curTimeText = " + _trackCurTime->jsRef() + ";\
|
||||||
|
document.lms.audio.playPause = " + playPauseBtn->jsRef() + ";\
|
||||||
|
\
|
||||||
|
document.lms.audio.offset = 0;\
|
||||||
|
document.lms.audio.curTime = 0;\
|
||||||
|
document.lms.audio.state = \"init\";\
|
||||||
|
document.lms.audio.volume = 1;\
|
||||||
|
\
|
||||||
|
document.lms.audio.seekbar.value = 0;\
|
||||||
|
document.lms.audio.seekbar.disabled = true;\
|
||||||
|
\
|
||||||
|
document.lms.audio.volumeSlider.min = 0;\
|
||||||
|
document.lms.audio.volumeSlider.max = 100;\
|
||||||
|
document.lms.audio.volumeSlider.value = 100;\
|
||||||
|
\
|
||||||
|
function updateUIPlaying() { \
|
||||||
|
var icon = document.lms.audio.playPause.getElementsByTagName(\"i\")[0]; \
|
||||||
|
icon.className = \"fa fa-pause fa-3x fa-fw\"; \
|
||||||
|
} \
|
||||||
|
\
|
||||||
|
function updateUIStopped() { \
|
||||||
|
var icon = document.lms.audio.playPause.getElementsByTagName(\"i\")[0]; \
|
||||||
|
icon.className = \"fa fa-play fa-3x fa-fw\"; \
|
||||||
|
} \
|
||||||
|
\
|
||||||
|
function durationToString(duration, displayHours) { \
|
||||||
|
var hours = parseInt( duration / 3600 ) % 24; \
|
||||||
|
var minutes = parseInt( duration / 60) % 60; \
|
||||||
|
var seconds = duration % 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; \
|
||||||
|
} \
|
||||||
|
\
|
||||||
|
function updateUI() {\
|
||||||
|
document.lms.audio.curTimeText.innerHTML = durationToString(document.lms.audio.curTime, document.lms.audio.seekbar.max > 3600); \
|
||||||
|
document.lms.audio.seekbar.value = document.lms.audio.curTime;\
|
||||||
|
}\
|
||||||
|
\
|
||||||
|
var mouseDown = 0;\
|
||||||
|
function seekMouseDown(e) {\
|
||||||
|
++mouseDown;\
|
||||||
|
}\
|
||||||
|
function seekMouseUp(e) {\
|
||||||
|
--mouseDown;\
|
||||||
|
}\
|
||||||
|
\
|
||||||
|
function seeking(e) {\
|
||||||
|
if (document.lms.audio.state == \"init\")\
|
||||||
|
return;\
|
||||||
|
\
|
||||||
|
document.lms.audio.curTimeText.innerHTML = durationToString(document.lms.audio.seekbar.value, document.lms.audio.seekbar.max > 3600); \
|
||||||
|
}\
|
||||||
|
\
|
||||||
|
function seek(e) {\
|
||||||
|
if (document.lms.audio.state == \"init\")\
|
||||||
|
return;\
|
||||||
|
\
|
||||||
|
document.lms.audio.audio.pause(); \
|
||||||
|
document.lms.audio.offset = parseInt(document.lms.audio.seekbar.value);\
|
||||||
|
document.lms.audio.curTime = document.lms.audio.seekbar.value;\
|
||||||
|
var audioSource = document.lms.audio.audio.getElementsByTagName(\"source\")[0];\
|
||||||
|
var src = audioSource.src;\
|
||||||
|
src = src.slice(0, src.lastIndexOf(\"=\") + 1);\
|
||||||
|
audioSource.src = src + document.lms.audio.seekbar.value;\
|
||||||
|
document.lms.audio.audio.load(); \
|
||||||
|
document.lms.audio.audio.play(); \
|
||||||
|
document.lms.audio.curTimeText.innerHTML = durationToString(document.lms.audio.curTime, document.lms.audio.seekbar.max > 3600); \
|
||||||
|
}\
|
||||||
|
\
|
||||||
|
function volumeChanged() {\
|
||||||
|
document.lms.audio.audio.volume = document.lms.audio.volumeSlider.value / 100;\
|
||||||
|
}\
|
||||||
|
\
|
||||||
|
function updateCurTime() {\
|
||||||
|
document.lms.audio.curTime = document.lms.audio.offset + ~~document.lms.audio.audio.currentTime; \
|
||||||
|
if (mouseDown == 0)\
|
||||||
|
updateUI();\
|
||||||
|
} \
|
||||||
|
\
|
||||||
|
function playPause() { \
|
||||||
|
if (document.lms.audio.state == \"init\") \
|
||||||
|
return;\
|
||||||
|
\
|
||||||
|
if (document.lms.audio.audio.paused) \
|
||||||
|
document.lms.audio.audio.play(); \
|
||||||
|
else \
|
||||||
|
document.lms.audio.audio.pause();\
|
||||||
|
\
|
||||||
|
}\
|
||||||
|
\
|
||||||
|
document.lms.audio.audio.addEventListener('timeupdate', updateCurTime); \
|
||||||
|
document.lms.audio.audio.addEventListener('playing', updateUIPlaying); \
|
||||||
|
document.lms.audio.audio.addEventListener('play', updateUIPlaying); \
|
||||||
|
document.lms.audio.audio.addEventListener('pause', updateUIStopped); \
|
||||||
|
document.lms.audio.audio.addEventListener('ended', updateUIStopped); \
|
||||||
|
document.lms.audio.seekbar.addEventListener('change', seek);\
|
||||||
|
document.lms.audio.seekbar.addEventListener('input', seeking);\
|
||||||
|
document.lms.audio.seekbar.addEventListener('mousedown', seekMouseDown);\
|
||||||
|
document.lms.audio.seekbar.addEventListener('mouseup', seekMouseUp);\
|
||||||
|
document.lms.audio.volumeSlider.addEventListener('input', volumeChanged);\
|
||||||
|
document.lms.audio.playPause.addEventListener('click', playPause);\
|
||||||
|
"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} // namespaceUserInterface
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Wt/WTemplate>
|
||||||
|
#include <Wt/WSignal>
|
||||||
|
|
||||||
|
#include "common/InputRange.hpp"
|
||||||
|
#include "database/Types.hpp"
|
||||||
|
#include "av/AvTranscoder.hpp"
|
||||||
|
|
||||||
|
namespace UserInterface {
|
||||||
|
|
||||||
|
class AudioPlayer : public Wt::WTemplate
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
|
||||||
|
AudioPlayer(Wt::WContainerWidget *parent = 0);
|
||||||
|
|
||||||
|
Av::Encoding getBestEncoding() const;
|
||||||
|
bool loadTrack(Database::Track::id_type trackId);
|
||||||
|
|
||||||
|
// Signals
|
||||||
|
Wt::Signal<void>& playbackEnded() {return _playbackEnded;}
|
||||||
|
Wt::Signal<void>& playNext() {return _playNext;}
|
||||||
|
Wt::Signal<void>& playPrevious() {return _playPrevious;}
|
||||||
|
Wt::Signal<bool>& shuffle() {return _shuffle;}
|
||||||
|
Wt::Signal<bool>& loop() {return _loop;}
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
// Signals
|
||||||
|
Wt::Signal<void> _playbackEnded;
|
||||||
|
Wt::Signal<void> _playNext;
|
||||||
|
Wt::Signal<void> _playPrevious;
|
||||||
|
Wt::Signal<bool> _shuffle;
|
||||||
|
Wt::Signal<bool> _loop;
|
||||||
|
|
||||||
|
Wt::WAudio* _audio;
|
||||||
|
Wt::WText* _trackCurTime;
|
||||||
|
Wt::WText* _trackDuration;
|
||||||
|
Wt::WText* _trackName;
|
||||||
|
Wt::WImage* _cover;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace UserInterface
|
||||||
@@ -1,292 +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 <Wt/WMediaPlayer>
|
|
||||||
#include <Wt/WProgressBar>
|
|
||||||
#include <Wt/WCheckBox>
|
|
||||||
#include <Wt/WTemplate>
|
|
||||||
#include <Wt/WHBoxLayout>
|
|
||||||
#include <Wt/WVBoxLayout>
|
|
||||||
|
|
||||||
#include "logger/Logger.hpp"
|
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
|
|
||||||
#include "AudioMediaPlayer.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Desktop {
|
|
||||||
|
|
||||||
Wt::WMediaPlayer::Encoding
|
|
||||||
AudioMediaPlayer::getBestEncoding()
|
|
||||||
{
|
|
||||||
// MP3 seems to be better supported everywhere
|
|
||||||
return Wt::WMediaPlayer::MP3;
|
|
||||||
}
|
|
||||||
|
|
||||||
AudioMediaPlayer::AudioMediaPlayer(Wt::WContainerWidget *parent)
|
|
||||||
: Wt::WContainerWidget(parent),
|
|
||||||
_mediaResource(nullptr)
|
|
||||||
{
|
|
||||||
this->setStyleClass("mediaplayer");
|
|
||||||
|
|
||||||
Wt::WVBoxLayout* mainLayout = new Wt::WVBoxLayout();
|
|
||||||
this->setLayout(mainLayout);
|
|
||||||
|
|
||||||
// Determine the encoding to be used
|
|
||||||
{
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
switch (CurrentUser()->getAudioEncoding())
|
|
||||||
{
|
|
||||||
case Database::AudioEncoding::MP3: _encoding = Wt::WMediaPlayer::MP3; break;
|
|
||||||
case Database::AudioEncoding::WEBMA: _encoding = Wt::WMediaPlayer::WEBMA; break;
|
|
||||||
case Database::AudioEncoding::OGA: _encoding = Wt::WMediaPlayer::OGA; break;
|
|
||||||
case Database::AudioEncoding::FLA: _encoding = Wt::WMediaPlayer::FLA; break;
|
|
||||||
case Database::AudioEncoding::AUTO:
|
|
||||||
default:
|
|
||||||
_encoding = getBestEncoding();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LMS_LOG(UI, INFO) << "Audio player using encoding " << _encoding;
|
|
||||||
|
|
||||||
// Current Media info
|
|
||||||
Wt::WHBoxLayout *currentMediaLayout = new Wt::WHBoxLayout();
|
|
||||||
mainLayout->addLayout(currentMediaLayout, 1);
|
|
||||||
|
|
||||||
currentMediaLayout->addWidget( _mediaCover = new Wt::WImage());
|
|
||||||
_mediaCover->setImageLink( LmsApplication::instance()->getCoverResource()->getUnknownTrackUrl(72));
|
|
||||||
_mediaCover->setStyleClass("mediaplayer-current-cover");
|
|
||||||
|
|
||||||
Wt::WVBoxLayout* mediaInfoLayout = new Wt::WVBoxLayout();
|
|
||||||
currentMediaLayout->addLayout(mediaInfoLayout, 1);
|
|
||||||
|
|
||||||
mediaInfoLayout->addWidget( _mediaTitle = new Wt::WText("---"));
|
|
||||||
mediaInfoLayout->addWidget( _mediaArtistRelease = new Wt::WText("---"));
|
|
||||||
_mediaTitle->setStyleClass("mediaplayer-current-track vertical-align");
|
|
||||||
_mediaArtistRelease->setStyleClass("mediaplayer-current-artist vertical-align");
|
|
||||||
|
|
||||||
// Time control
|
|
||||||
Wt::WHBoxLayout *sliderLayout = new Wt::WHBoxLayout();
|
|
||||||
mainLayout->addLayout(sliderLayout);
|
|
||||||
|
|
||||||
sliderLayout->addWidget(_curTime = new Wt::WText("00:00:00"));
|
|
||||||
sliderLayout->addWidget(_timeSlider = new Wt::WSlider( ), 1);
|
|
||||||
sliderLayout->addWidget(_duration = new Wt::WText("00:00:00"));
|
|
||||||
_timeSlider->setHeight(26); // Default is too big (50)
|
|
||||||
_curTime->setStyleClass("vertical-align");
|
|
||||||
_duration->setStyleClass("vertical-align");
|
|
||||||
|
|
||||||
// Controls
|
|
||||||
Wt::WHBoxLayout *controlsLayout = new Wt::WHBoxLayout();
|
|
||||||
mainLayout->addLayout(controlsLayout);
|
|
||||||
|
|
||||||
|
|
||||||
Wt::WPushButton *prevBtn = new Wt::WPushButton("<<");
|
|
||||||
controlsLayout->addWidget(prevBtn);
|
|
||||||
prevBtn->setStyleClass("mediaplayer-btn-controls");
|
|
||||||
|
|
||||||
Wt::WContainerWidget *btnContainer = new Wt::WContainerWidget();;
|
|
||||||
|
|
||||||
_playBtn = new Wt::WPushButton("Play");
|
|
||||||
btnContainer->addWidget(_playBtn);
|
|
||||||
_playBtn->setWidth(70);
|
|
||||||
_playBtn->setStyleClass("mediaplayer-btn-controls");
|
|
||||||
|
|
||||||
_pauseBtn = new Wt::WPushButton("Pause");
|
|
||||||
btnContainer->addWidget(_pauseBtn);
|
|
||||||
_pauseBtn->setWidth(70);
|
|
||||||
_pauseBtn->setStyleClass("mediaplayer-btn-controls");
|
|
||||||
|
|
||||||
controlsLayout->addWidget(btnContainer);
|
|
||||||
|
|
||||||
Wt::WPushButton *nextBtn = new Wt::WPushButton(">>");
|
|
||||||
controlsLayout->addWidget(nextBtn);
|
|
||||||
nextBtn->setStyleClass("mediaplayer-btn-controls");
|
|
||||||
|
|
||||||
_volumeSlider = new Wt::WSlider();
|
|
||||||
_volumeSlider->setRange(0,100);
|
|
||||||
_volumeSlider->setWidth(60); // Default is too big (150)
|
|
||||||
_volumeSlider->setHeight(26); // Default is too big (50)
|
|
||||||
_volumeSlider->setMinimumSize(50, Wt::WLength::Auto);
|
|
||||||
controlsLayout->addWidget(_volumeSlider, 1);
|
|
||||||
|
|
||||||
Wt::WPushButton *loop = new Wt::WPushButton("Loop");
|
|
||||||
loop->setCheckable(true);
|
|
||||||
loop->setStyleClass("btn-xs");
|
|
||||||
loop->checked().connect(std::bind([=] () { _loop.emit( true ); }));
|
|
||||||
loop->unChecked().connect(std::bind([=] () { _loop.emit( false ); }));
|
|
||||||
controlsLayout->addWidget(loop);
|
|
||||||
|
|
||||||
Wt::WPushButton *shuffle = new Wt::WPushButton("Shuffle");
|
|
||||||
shuffle->setCheckable(true);
|
|
||||||
shuffle->setStyleClass("btn-xs");
|
|
||||||
shuffle->checked().connect(std::bind([=] () { _shuffle.emit( true ); }));
|
|
||||||
shuffle->unChecked().connect(std::bind([=] () { _shuffle.emit( false );}));
|
|
||||||
controlsLayout->addWidget(shuffle);
|
|
||||||
|
|
||||||
_mediaPlayer = new Wt::WMediaPlayer( Wt::WMediaPlayer::Audio, btnContainer );
|
|
||||||
_mediaPlayer->addSource( _encoding, "" );
|
|
||||||
_mediaPlayer->ended().connect(this, &AudioMediaPlayer::handleTrackEnded);
|
|
||||||
|
|
||||||
_mediaPlayer->setControlsWidget( 0 );
|
|
||||||
_mediaPlayer->setButton(Wt::WMediaPlayer::Play, _playBtn);
|
|
||||||
_mediaPlayer->setButton(Wt::WMediaPlayer::Pause, _pauseBtn);
|
|
||||||
|
|
||||||
_mediaPlayer->timeUpdated().connect(this, &AudioMediaPlayer::handleTimeUpdated);
|
|
||||||
|
|
||||||
_volumeSlider->setValue(_mediaPlayer->volume() * 100);
|
|
||||||
|
|
||||||
nextBtn->clicked().connect(std::bind([=] ()
|
|
||||||
{
|
|
||||||
_mediaPlayer->stop();
|
|
||||||
_playNext.emit();
|
|
||||||
}));
|
|
||||||
|
|
||||||
prevBtn->clicked().connect(std::bind([=] ()
|
|
||||||
{
|
|
||||||
_mediaPlayer->stop();
|
|
||||||
_playPrevious.emit();
|
|
||||||
}));
|
|
||||||
_timeSlider->valueChanged().connect(this, &AudioMediaPlayer::handlePlayOffset);
|
|
||||||
_timeSlider->setDisabled(true);
|
|
||||||
|
|
||||||
_volumeSlider->sliderMoved().connect(this, &AudioMediaPlayer::handleVolumeSliderMoved);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
AudioMediaPlayer::loadPlayer(boost::filesystem::path filePath, Av::TranscodeParameters& parameters)
|
|
||||||
{
|
|
||||||
_currentFile = filePath;
|
|
||||||
_currentParameters = parameters;
|
|
||||||
|
|
||||||
_mediaPlayer->clearSources();
|
|
||||||
|
|
||||||
if (_mediaResource)
|
|
||||||
delete _mediaResource;
|
|
||||||
|
|
||||||
_mediaResource = new AvConvTranscodeStreamResource( filePath, parameters, this );
|
|
||||||
|
|
||||||
_mediaPlayer->addSource( getEncoding(), Wt::WLink(_mediaResource));
|
|
||||||
|
|
||||||
// Auto play
|
|
||||||
_mediaPlayer->play();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
AudioMediaPlayer::load(Database::Track::id_type trackId)
|
|
||||||
{
|
|
||||||
Av::TranscodeParameters parameters;
|
|
||||||
|
|
||||||
boost::filesystem::path path;
|
|
||||||
|
|
||||||
boost::posix_time::time_duration duration;
|
|
||||||
|
|
||||||
{
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
Database::Track::pointer track = Database::Track::getById(DboSession(), trackId);
|
|
||||||
if (!track)
|
|
||||||
{
|
|
||||||
LMS_LOG(UI, INFO) << "Cannot find track id " << trackId;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
path = track->getPath();
|
|
||||||
parameters.setBitrate(Av::Stream::Type::Audio, CurrentUser()->getAudioBitrate() );
|
|
||||||
|
|
||||||
duration = track->getDuration();
|
|
||||||
|
|
||||||
_mediaTitle->setText ( Wt::WString::fromUTF8(track->getName()) );
|
|
||||||
_mediaArtistRelease->setText ( Wt::WString::fromUTF8(track->getArtist()->getName()) + " - " + Wt::WString::fromUTF8(track->getRelease()->getName()) );
|
|
||||||
_mediaCover->setImageLink( Wt::WLink (LmsApplication::instance()->getCoverResource()->getTrackUrl(trackId, 72)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Av::MediaFile mediaFile(path);
|
|
||||||
|
|
||||||
if (!mediaFile.open() || !mediaFile.scan())
|
|
||||||
{
|
|
||||||
LMS_LOG(UI, INFO) << "Cannot open file '" << path << "'";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// It seems to be far better to manually map the streams
|
|
||||||
// otherwise, some files may have to be fully transcoded to be played by browser...
|
|
||||||
int audioBestStreamId = mediaFile.getBestStreamId(Av::Stream::Type::Audio);
|
|
||||||
if (audioBestStreamId != -1)
|
|
||||||
parameters.addStream(audioBestStreamId);
|
|
||||||
|
|
||||||
Av::Encoding encoding;
|
|
||||||
switch (_encoding)
|
|
||||||
{
|
|
||||||
case Wt::WMediaPlayer::MP3: encoding = Av::Encoding::MP3; break;
|
|
||||||
case Wt::WMediaPlayer::FLA: encoding = Av::Encoding::FLA; break;
|
|
||||||
case Wt::WMediaPlayer::OGA: encoding = Av::Encoding::OGA; break;
|
|
||||||
case Wt::WMediaPlayer::WEBMA: encoding = Av::Encoding::WEBMA; break;
|
|
||||||
default:
|
|
||||||
encoding = Av::Encoding::MP3;
|
|
||||||
}
|
|
||||||
parameters.setEncoding(encoding);
|
|
||||||
|
|
||||||
_timeSlider->setDisabled(false);
|
|
||||||
_timeSlider->setRange(0, duration.total_seconds() );
|
|
||||||
_timeSlider->setValue(0);
|
|
||||||
|
|
||||||
_duration->setText( boost::posix_time::to_simple_string( duration ));
|
|
||||||
|
|
||||||
loadPlayer(path, parameters);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
AudioMediaPlayer::handlePlayOffset(int offsetSecs)
|
|
||||||
{
|
|
||||||
Av::TranscodeParameters parameters = _currentParameters;
|
|
||||||
|
|
||||||
parameters.setOffset( boost::posix_time::seconds(offsetSecs) );
|
|
||||||
|
|
||||||
loadPlayer(_currentFile, parameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
AudioMediaPlayer::handleTrackEnded(void)
|
|
||||||
{
|
|
||||||
LMS_LOG(UI, INFO) << "Track ended!";
|
|
||||||
_playbackEnded.emit();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
AudioMediaPlayer::handleTimeUpdated(void)
|
|
||||||
{
|
|
||||||
boost::posix_time::time_duration currentTime ( boost::posix_time::seconds( _mediaPlayer->currentTime() + _currentParameters.getOffset().total_seconds()));
|
|
||||||
|
|
||||||
_timeSlider->setValue( currentTime.total_seconds() );
|
|
||||||
_curTime->setText( boost::posix_time::to_simple_string( currentTime) );
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
AudioMediaPlayer::handleVolumeSliderMoved(int value)
|
|
||||||
{
|
|
||||||
_mediaPlayer->setVolume( value / 100. );
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Desktop
|
|
||||||
} // namespace UserInterface
|
|
||||||
@@ -1,110 +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 __AUDIO_MEDIA_PLAYER_HPP
|
|
||||||
#define __AUDIO_MEDIA_PLAYER_HPP
|
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
#include <Wt/WSlider>
|
|
||||||
#include <Wt/WPushButton>
|
|
||||||
#include <Wt/WContainerWidget>
|
|
||||||
#include <Wt/WMediaPlayer>
|
|
||||||
#include <Wt/WText>
|
|
||||||
#include <Wt/WImage>
|
|
||||||
|
|
||||||
#include "database/Types.hpp"
|
|
||||||
|
|
||||||
#include "av/AvTranscoder.hpp"
|
|
||||||
#include "resource/AvConvTranscodeStreamResource.hpp"
|
|
||||||
#include "resource/CoverResource.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Desktop {
|
|
||||||
|
|
||||||
class AudioMediaPlayer : public Wt::WContainerWidget
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
static Wt::WMediaPlayer::Encoding getBestEncoding();
|
|
||||||
|
|
||||||
AudioMediaPlayer(Wt::WContainerWidget *parent = 0);
|
|
||||||
|
|
||||||
// Load Media to be played
|
|
||||||
bool load(Database::Track::id_type trackId);
|
|
||||||
|
|
||||||
// Accessors
|
|
||||||
Wt::WMediaPlayer::Encoding getEncoding() const { return _encoding; }
|
|
||||||
|
|
||||||
// Signal slots
|
|
||||||
Wt::Signal<void>& playbackEnded() {return _playbackEnded;}
|
|
||||||
Wt::Signal<void>& playNext() {return _playNext;}
|
|
||||||
Wt::Signal<void>& playPrevious() {return _playPrevious;}
|
|
||||||
Wt::Signal<bool>& shuffle() {return _shuffle;}
|
|
||||||
Wt::Signal<bool>& loop() {return _loop;}
|
|
||||||
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
void handlePlayOffset(int offsetSecs);
|
|
||||||
void handleTrackEnded(void);
|
|
||||||
|
|
||||||
void handleValueChanged(double);
|
|
||||||
void handleTimeUpdated(void);
|
|
||||||
void handleSliderMoved(int value);
|
|
||||||
|
|
||||||
void handleVolumeSliderMoved(int value);
|
|
||||||
|
|
||||||
void loadPlayer(boost::filesystem::path filePath, Av::TranscodeParameters& parameters);
|
|
||||||
|
|
||||||
// Signals
|
|
||||||
Wt::Signal<void> _playbackEnded;
|
|
||||||
Wt::Signal<void> _playNext;
|
|
||||||
Wt::Signal<void> _playPrevious;
|
|
||||||
Wt::Signal<bool> _shuffle;
|
|
||||||
Wt::Signal<bool> _loop;
|
|
||||||
|
|
||||||
// Core
|
|
||||||
Wt::WMediaPlayer* _mediaPlayer;
|
|
||||||
AvConvTranscodeStreamResource* _mediaResource;
|
|
||||||
Wt::WMediaPlayer::Encoding _encoding;
|
|
||||||
|
|
||||||
// Media info
|
|
||||||
Wt::WImage* _mediaCover;
|
|
||||||
Wt::WText* _mediaTitle;
|
|
||||||
Wt::WText* _mediaArtistRelease;
|
|
||||||
|
|
||||||
// Controls
|
|
||||||
Wt::WPushButton* _playBtn;
|
|
||||||
Wt::WPushButton* _pauseBtn;
|
|
||||||
Wt::WSlider* _timeSlider;
|
|
||||||
Wt::WSlider* _volumeSlider;
|
|
||||||
Wt::WText* _curTime;
|
|
||||||
Wt::WText* _duration;
|
|
||||||
|
|
||||||
// Transcode
|
|
||||||
boost::filesystem::path _currentFile;
|
|
||||||
Av::TranscodeParameters _currentParameters;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace Desktop
|
|
||||||
} // namespace UserInterface
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
@@ -120,73 +120,72 @@ _playQueue(nullptr)
|
|||||||
_playQueue = new PlayQueue();
|
_playQueue = new PlayQueue();
|
||||||
|
|
||||||
// Playlist/PlayQueue
|
// Playlist/PlayQueue
|
||||||
|
Wt::WContainerWidget* playQueueContainer = new Wt::WContainerWidget();
|
||||||
|
playQueueContainer->setStyleClass("playqueue");
|
||||||
|
Wt::WVBoxLayout* playQueueLayout = new Wt::WVBoxLayout();
|
||||||
|
playQueueContainer->setLayout(playQueueLayout);
|
||||||
|
|
||||||
|
playQueueLayout->addWidget( _playQueue, 1);
|
||||||
|
|
||||||
|
Wt::WHBoxLayout* playlistControls = new Wt::WHBoxLayout();
|
||||||
|
|
||||||
|
Wt::WPushButton *playlistBtn = new Wt::WPushButton("Playlist");
|
||||||
|
playlistBtn->setStyleClass("btn-sm btn-primary");
|
||||||
|
playlistControls->addWidget(playlistBtn);
|
||||||
|
|
||||||
|
// Playlist menu
|
||||||
|
{
|
||||||
|
Wt::WPopupMenu *popupMain = new Wt::WPopupMenu();
|
||||||
|
|
||||||
|
_popupMenuSave = new Wt::WPopupMenu();
|
||||||
|
popupMain->addMenu("Save", _popupMenuSave);
|
||||||
|
|
||||||
|
_popupMenuLoad = new Wt::WPopupMenu();
|
||||||
|
popupMain->addMenu("Load", _popupMenuLoad);
|
||||||
|
|
||||||
|
_popupMenuDelete = new Wt::WPopupMenu();
|
||||||
|
popupMain->addMenu("Delete", _popupMenuDelete);
|
||||||
|
|
||||||
|
playlistBtn->setMenu(popupMain);
|
||||||
|
}
|
||||||
|
|
||||||
|
Wt::WPushButton *upBtn = new Wt::WPushButton("UP");
|
||||||
|
upBtn->setStyleClass("btn-sm");
|
||||||
|
playlistControls->addWidget(upBtn);
|
||||||
|
Wt::WPushButton *downBtn = new Wt::WPushButton("DO");
|
||||||
|
downBtn->setStyleClass("btn-sm");
|
||||||
|
playlistControls->addWidget(downBtn);
|
||||||
|
Wt::WPushButton *delBtn = new Wt::WPushButton("DEL");
|
||||||
|
delBtn->setStyleClass("btn-sm btn-warning");
|
||||||
|
playlistControls->addWidget(delBtn);
|
||||||
|
Wt::WPushButton *clearBtn = new Wt::WPushButton("CLR");
|
||||||
|
clearBtn->setStyleClass("btn-sm btn-danger");
|
||||||
|
playlistControls->addWidget(clearBtn);
|
||||||
|
|
||||||
|
delBtn->clicked().connect(_playQueue, &PlayQueue::delSelected);
|
||||||
|
upBtn->clicked().connect(_playQueue, &PlayQueue::moveSelectedUp);
|
||||||
|
downBtn->clicked().connect(_playQueue, &PlayQueue::moveSelectedDown);
|
||||||
|
clearBtn->clicked().connect(_playQueue, &PlayQueue::delAll);
|
||||||
|
|
||||||
|
playQueueLayout->addLayout(playlistControls);
|
||||||
|
|
||||||
|
mainLayout->addWidget(playQueueContainer, 0, 0, 2, 1);
|
||||||
|
|
||||||
|
// Load the last known queue
|
||||||
|
playlistLoadToPlayqueue(CurrentQueuePlaylistName);
|
||||||
|
|
||||||
|
// Select the last known playing track
|
||||||
{
|
{
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
Wt::Dbo::Transaction transaction(DboSession());
|
||||||
|
|
||||||
Wt::WContainerWidget* playQueueContainer = new Wt::WContainerWidget();
|
|
||||||
playQueueContainer->setStyleClass("playqueue");
|
|
||||||
Wt::WVBoxLayout* playQueueLayout = new Wt::WVBoxLayout();
|
|
||||||
playQueueContainer->setLayout(playQueueLayout);
|
|
||||||
|
|
||||||
_mediaPlayer = new AudioMediaPlayer();
|
|
||||||
playQueueLayout->addWidget(_mediaPlayer);
|
|
||||||
|
|
||||||
playQueueLayout->addWidget( _playQueue, 1);
|
|
||||||
|
|
||||||
Wt::WHBoxLayout* playlistControls = new Wt::WHBoxLayout();
|
|
||||||
|
|
||||||
Wt::WPushButton *playlistBtn = new Wt::WPushButton("Playlist");
|
|
||||||
playlistBtn->setStyleClass("btn-sm btn-primary");
|
|
||||||
playlistControls->addWidget(playlistBtn);
|
|
||||||
|
|
||||||
// Playlist menu
|
|
||||||
{
|
|
||||||
Wt::WPopupMenu *popupMain = new Wt::WPopupMenu();
|
|
||||||
|
|
||||||
_popupMenuSave = new Wt::WPopupMenu();
|
|
||||||
popupMain->addMenu("Save", _popupMenuSave);
|
|
||||||
|
|
||||||
_popupMenuLoad = new Wt::WPopupMenu();
|
|
||||||
popupMain->addMenu("Load", _popupMenuLoad);
|
|
||||||
|
|
||||||
_popupMenuDelete = new Wt::WPopupMenu();
|
|
||||||
popupMain->addMenu("Delete", _popupMenuDelete);
|
|
||||||
|
|
||||||
playlistBtn->setMenu(popupMain);
|
|
||||||
}
|
|
||||||
|
|
||||||
Wt::WPushButton *upBtn = new Wt::WPushButton("UP");
|
|
||||||
upBtn->setStyleClass("btn-sm");
|
|
||||||
playlistControls->addWidget(upBtn);
|
|
||||||
Wt::WPushButton *downBtn = new Wt::WPushButton("DO");
|
|
||||||
downBtn->setStyleClass("btn-sm");
|
|
||||||
playlistControls->addWidget(downBtn);
|
|
||||||
Wt::WPushButton *delBtn = new Wt::WPushButton("DEL");
|
|
||||||
delBtn->setStyleClass("btn-sm btn-warning");
|
|
||||||
playlistControls->addWidget(delBtn);
|
|
||||||
Wt::WPushButton *clearBtn = new Wt::WPushButton("CLR");
|
|
||||||
clearBtn->setStyleClass("btn-sm btn-danger");
|
|
||||||
playlistControls->addWidget(clearBtn);
|
|
||||||
|
|
||||||
delBtn->clicked().connect(_playQueue, &PlayQueue::delSelected);
|
|
||||||
upBtn->clicked().connect(_playQueue, &PlayQueue::moveSelectedUp);
|
|
||||||
downBtn->clicked().connect(_playQueue, &PlayQueue::moveSelectedDown);
|
|
||||||
clearBtn->clicked().connect(_playQueue, &PlayQueue::delAll);
|
|
||||||
|
|
||||||
|
|
||||||
playQueueLayout->addLayout(playlistControls);
|
|
||||||
|
|
||||||
mainLayout->addWidget(playQueueContainer, 0, 0, 2, 1);
|
|
||||||
|
|
||||||
// Load the last known queue
|
|
||||||
playlistLoadToPlayqueue(CurrentQueuePlaylistName);
|
|
||||||
// Select the last known playing track
|
|
||||||
_playQueue->select(CurrentUser()->getCurPlayingTrackPos());
|
_playQueue->select(CurrentUser()->getCurPlayingTrackPos());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_mediaPlayer = new AudioPlayer();
|
||||||
|
mainLayout->addWidget(_mediaPlayer, 2, 0, 1, 4);
|
||||||
|
|
||||||
mainLayout->setRowStretch(1, 1);
|
mainLayout->setRowStretch(1, 1);
|
||||||
mainLayout->setRowResizable(0, true, Wt::WLength(250, Wt::WLength::Pixel));
|
mainLayout->setRowResizable(0, true, Wt::WLength(250, Wt::WLength::Pixel));
|
||||||
mainLayout->setColumnResizable(0, true);
|
mainLayout->setColumnResizable(0, true, Wt::WLength(400, Wt::WLength::Pixel));
|
||||||
|
|
||||||
// Double click on track
|
// Double click on track
|
||||||
// Set the selected tracks to the play queue
|
// Set the selected tracks to the play queue
|
||||||
@@ -510,7 +509,7 @@ Audio::playTrack(Track::id_type trackId, int pos)
|
|||||||
CurrentUser().modify()->setCurPlayingTrackPos(pos);
|
CurrentUser().modify()->setCurPlayingTrackPos(pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_mediaPlayer->load(trackId))
|
if (!_mediaPlayer->loadTrack(trackId))
|
||||||
_playQueue->playNext();
|
_playQueue->playNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,14 +17,13 @@
|
|||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef UI_AUDIO_DESKTOP_HPP
|
#pragma once
|
||||||
#define UI_AUDIO_DESKTOP_HPP
|
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include <Wt/WPopupMenu>
|
#include <Wt/WPopupMenu>
|
||||||
|
|
||||||
#include "AudioMediaPlayer.hpp"
|
#include "audio/AudioPlayer.hpp"
|
||||||
#include "TrackView.hpp"
|
#include "TrackView.hpp"
|
||||||
#include "PlayQueue.hpp"
|
#include "PlayQueue.hpp"
|
||||||
|
|
||||||
@@ -66,7 +65,7 @@ class Audio : public UserInterface::Audio
|
|||||||
|
|
||||||
void handlePlaylistSelected(Wt::WString name);
|
void handlePlaylistSelected(Wt::WString name);
|
||||||
|
|
||||||
AudioMediaPlayer* _mediaPlayer;
|
AudioPlayer* _mediaPlayer;
|
||||||
TrackView* _trackView;
|
TrackView* _trackView;
|
||||||
PlayQueue* _playQueue;
|
PlayQueue* _playQueue;
|
||||||
|
|
||||||
@@ -80,5 +79,3 @@ class Audio : public UserInterface::Audio
|
|||||||
} // namespace Desktop
|
} // namespace Desktop
|
||||||
} // namespace UserInterface
|
} // namespace UserInterface
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
|||||||
@@ -278,10 +278,8 @@ _trackSelector(new TrackSelector())
|
|||||||
_model = new Wt::WStandardItemModel(0, 3, this);
|
_model = new Wt::WStandardItemModel(0, 3, this);
|
||||||
|
|
||||||
// 0 Column is hidden (track id)
|
// 0 Column is hidden (track id)
|
||||||
_model->setHeaderData(COLUMN_ID_TRACK_ID, Wt::WString("#"));
|
|
||||||
_model->setHeaderData(COLUMN_ID_COVER, Wt::WString("Cover"));
|
|
||||||
_model->setHeaderData(COLUMN_ID_NAME, Wt::WString("Track"));
|
|
||||||
|
|
||||||
|
this->setHeaderHeight(0);
|
||||||
this->setModel(_model);
|
this->setModel(_model);
|
||||||
this->setSelectionMode(Wt::ExtendedSelection);
|
this->setSelectionMode(Wt::ExtendedSelection);
|
||||||
this->setSortingEnabled(false);
|
this->setSortingEnabled(false);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
#include <Wt/WBreak>
|
#include <Wt/WBreak>
|
||||||
|
|
||||||
#include "logger/Logger.hpp"
|
#include "logger/Logger.hpp"
|
||||||
|
#include "utils/Utils.hpp"
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
#include "TrackView.hpp"
|
#include "TrackView.hpp"
|
||||||
@@ -41,18 +42,10 @@ class DurationItemDelegate : public Wt::WItemDelegate
|
|||||||
{
|
{
|
||||||
boost::posix_time::time_duration duration = boost::any_cast<boost::posix_time::time_duration>(index.data(Wt::DisplayRole));
|
boost::posix_time::time_duration duration = boost::any_cast<boost::posix_time::time_duration>(index.data(Wt::DisplayRole));
|
||||||
|
|
||||||
boost::posix_time::time_facet* facet = new boost::posix_time::time_facet();
|
// TODO bug when redrawn
|
||||||
|
std::string format = duration.total_seconds() < 3600 ? "%M:%S" : "%H:%M:%S";
|
||||||
|
|
||||||
if (duration.total_seconds() < 3600)
|
return new Wt::WText(durationToString(duration, format), Wt::PlainText);
|
||||||
facet->time_duration_format("%M:%S");
|
|
||||||
else
|
|
||||||
facet->time_duration_format("%H:%M:%S");
|
|
||||||
|
|
||||||
std::ostringstream oss;
|
|
||||||
oss.imbue(std::locale(oss.getloc(), facet));
|
|
||||||
oss << duration;
|
|
||||||
|
|
||||||
return new Wt::WText(oss.str(), Wt::PlainText);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
#include <Wt/WText>
|
#include <Wt/WText>
|
||||||
#include <Wt/WTemplate>
|
#include <Wt/WTemplate>
|
||||||
#include <Wt/WPushButton>
|
#include <Wt/WPushButton>
|
||||||
|
#include <Wt/WImage>
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
|
|
||||||
@@ -38,6 +39,11 @@ _resCount(0)
|
|||||||
title->setTemplateText(Wt::WString::tr("mobile-search-title"));
|
title->setTemplateText(Wt::WString::tr("mobile-search-title"));
|
||||||
|
|
||||||
title->bindWidget("text", new Wt::WText("Artists", Wt::PlainText));
|
title->bindWidget("text", new Wt::WText("Artists", Wt::PlainText));
|
||||||
|
|
||||||
|
Wt::WTemplate* artistWrapper = new Wt::WTemplate(this);
|
||||||
|
artistWrapper->setTemplateText(Wt::WString::tr("wa-artist-wrapper"));
|
||||||
|
_contents = new Wt::WContainerWidget();
|
||||||
|
artistWrapper->bindWidget("contents", _contents );
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -75,7 +81,13 @@ ArtistSearch::addResults(Database::SearchFilter filter, std::size_t nb)
|
|||||||
for (Artist::pointer artist : artists)
|
for (Artist::pointer artist : artists)
|
||||||
{
|
{
|
||||||
Wt::WTemplate* res = new Wt::WTemplate(this);
|
Wt::WTemplate* res = new Wt::WTemplate(this);
|
||||||
res->setTemplateText(Wt::WString::tr("mobile-artist-res"));
|
res->setTemplateText(Wt::WString::tr("wa-artist-res"));
|
||||||
|
|
||||||
|
Wt::WImage *artistImg = new Wt::WImage();
|
||||||
|
artistImg->setStyleClass("center-block"); // TODO move in css?
|
||||||
|
artistImg->setStyleClass("release_res_shadow release_img-responsive"); // TODO move in css?
|
||||||
|
|
||||||
|
res->bindWidget("gif", artistImg);
|
||||||
|
|
||||||
Wt::WText *text = new Wt::WText(Wt::WString::fromUTF8(artist->getName()), Wt::PlainText);
|
Wt::WText *text = new Wt::WText(Wt::WString::fromUTF8(artist->getName()), Wt::PlainText);
|
||||||
res->bindWidget("name", text);
|
res->bindWidget("name", text);
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
#ifndef UI_MOBILE_ARTIST_SEARCH_HPP
|
#ifndef UI_MOBILE_ARTIST_SEARCH_HPP
|
||||||
#define UI_MOBILE_ARTIST_SEARCH_HPP
|
#define UI_MOBILE_ARTIST_SEARCH_HPP
|
||||||
|
|
||||||
#include <boost/algorithm/string/split.hpp>
|
|
||||||
#include <Wt/WContainerWidget>
|
#include <Wt/WContainerWidget>
|
||||||
|
|
||||||
#include "database/Types.hpp"
|
#include "database/Types.hpp"
|
||||||
@@ -48,6 +47,7 @@ class ArtistSearch : public Wt::WContainerWidget
|
|||||||
void clear(void);
|
void clear(void);
|
||||||
void addResults(Database::SearchFilter filter, size_t nb);
|
void addResults(Database::SearchFilter filter, size_t nb);
|
||||||
|
|
||||||
|
Wt::WContainerWidget* _contents;
|
||||||
std::size_t _resCount;
|
std::size_t _resCount;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -17,32 +17,58 @@
|
|||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <boost/algorithm/string/split.hpp>
|
|
||||||
#include <boost/algorithm/string.hpp>
|
|
||||||
#include <boost/foreach.hpp>
|
|
||||||
|
|
||||||
#include <Wt/WContainerWidget>
|
#include <Wt/WContainerWidget>
|
||||||
#include <Wt/WTemplate>
|
#include <Wt/WTemplate>
|
||||||
#include <Wt/WLineEdit>
|
|
||||||
#include <Wt/WText>
|
#include <Wt/WText>
|
||||||
#include <Wt/WTable>
|
|
||||||
|
|
||||||
|
#include "logger/Logger.hpp"
|
||||||
|
#include "utils/Utils.hpp"
|
||||||
|
|
||||||
|
#include "audio/AudioPlayer.hpp"
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
|
|
||||||
#include "MobileAudio.hpp"
|
#include "MobileAudio.hpp"
|
||||||
|
|
||||||
#include "ArtistSearch.hpp"
|
#define SEARCH_NB_ITEMS 4
|
||||||
#include "ReleaseSearch.hpp"
|
|
||||||
#include "TrackSearch.hpp"
|
|
||||||
#include "MobileAudioMediaPlayer.hpp"
|
|
||||||
|
|
||||||
#include "logger/Logger.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface {
|
||||||
namespace Mobile {
|
namespace Mobile {
|
||||||
|
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
|
|
||||||
|
static void playTrack(AudioPlayer *audioPlayer, Database::Track::id_type trackId)
|
||||||
|
{
|
||||||
|
LMS_LOG(UI, DEBUG) << "Playing track id " << trackId;
|
||||||
|
|
||||||
|
Wt::Dbo::Transaction transaction(DboSession());
|
||||||
|
|
||||||
|
Track::pointer track = Track::getById(DboSession(), trackId);
|
||||||
|
|
||||||
|
if (track)
|
||||||
|
audioPlayer->loadTrack(track.id());
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
Audio::search(std::string text)
|
||||||
|
{
|
||||||
|
// When a new search is done, output some results from:
|
||||||
|
// Artist
|
||||||
|
// Release
|
||||||
|
// Song
|
||||||
|
auto keywords = splitString(text, " ");;
|
||||||
|
|
||||||
|
_releaseSearch->search(SearchFilter::ByNameAnd(SearchFilter::Field::Release, keywords), SEARCH_NB_ITEMS);
|
||||||
|
_artistSearch->search(SearchFilter::ByNameAnd(SearchFilter::Field::Artist, keywords), SEARCH_NB_ITEMS);
|
||||||
|
_trackSearch->search(SearchFilter::ByNameAnd(SearchFilter::Field::Track, keywords), SEARCH_NB_ITEMS);
|
||||||
|
|
||||||
|
_artistSearch->show();
|
||||||
|
_releaseSearch->show();
|
||||||
|
_trackSearch->show();
|
||||||
|
_trackReleaseView->hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Audio::Audio(Wt::WContainerWidget *parent)
|
Audio::Audio(Wt::WContainerWidget *parent)
|
||||||
: UserInterface::Audio(parent)
|
: UserInterface::Audio(parent)
|
||||||
{
|
{
|
||||||
@@ -50,145 +76,78 @@ Audio::Audio(Wt::WContainerWidget *parent)
|
|||||||
this->setStyleClass("container-fluid");
|
this->setStyleClass("container-fluid");
|
||||||
this->setPadding(60, Wt::Bottom);
|
this->setPadding(60, Wt::Bottom);
|
||||||
|
|
||||||
Wt::WTemplate* search = new Wt::WTemplate(this);
|
_artistSearch = new ArtistSearch(this);
|
||||||
search->setTemplateText(Wt::WString::tr("mobile-search"));
|
_releaseSearch = new ReleaseSearch(this);
|
||||||
|
_trackSearch = new TrackSearch(this);
|
||||||
|
|
||||||
Wt::WLineEdit *edit = new Wt::WLineEdit();
|
_trackReleaseView = new TrackReleaseView(this);
|
||||||
edit->setEmptyText("Search...");
|
_trackReleaseView->hide();
|
||||||
|
|
||||||
search->bindWidget("search", edit);
|
|
||||||
search->setMargin(10);
|
|
||||||
|
|
||||||
ArtistSearch* artistSearch = new ArtistSearch(this);
|
|
||||||
ReleaseSearch* releaseSearch = new ReleaseSearch(this);
|
|
||||||
TrackSearch* trackSearch = new TrackSearch(this);
|
|
||||||
|
|
||||||
Wt::WTemplate* footer = new Wt::WTemplate(this);
|
Wt::WTemplate* footer = new Wt::WTemplate(this);
|
||||||
footer->setTemplateText(Wt::WString::tr("mobile-audio-footer"));
|
footer->setTemplateText(Wt::WString::tr("mobile-audio-footer"));
|
||||||
|
|
||||||
// Determine the encoding to be used
|
AudioPlayer* audioPlayer = new AudioPlayer();
|
||||||
Wt::WMediaPlayer::Encoding encoding;
|
footer->bindWidget("player", audioPlayer);
|
||||||
|
|
||||||
|
_artistSearch->moreArtistsSelected().connect(std::bind([=]
|
||||||
{
|
{
|
||||||
Wt::Dbo::Transaction transaction (DboSession());
|
_releaseSearch->hide();
|
||||||
|
_trackSearch->hide();
|
||||||
switch (CurrentUser()->getAudioEncoding())
|
_artistSearch->show();
|
||||||
{
|
_trackReleaseView->hide();
|
||||||
case AudioEncoding::MP3: encoding = Wt::WMediaPlayer::MP3; break;
|
|
||||||
case AudioEncoding::WEBMA: encoding = Wt::WMediaPlayer::WEBMA; break;
|
|
||||||
case AudioEncoding::OGA: encoding = Wt::WMediaPlayer::OGA; break;
|
|
||||||
case AudioEncoding::FLA: encoding = Wt::WMediaPlayer::FLA; break;
|
|
||||||
case AudioEncoding::AUTO:
|
|
||||||
default:
|
|
||||||
encoding = AudioMediaPlayer::getBestEncoding();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AudioMediaPlayer* mediaPlayer = new AudioMediaPlayer(encoding);
|
|
||||||
footer->bindWidget("player", mediaPlayer);
|
|
||||||
|
|
||||||
edit->changed().connect(std::bind([=] () {
|
|
||||||
std::string text = edit->text().toUTF8();
|
|
||||||
|
|
||||||
// When a new search is done, output some results from:
|
|
||||||
// Artist
|
|
||||||
// Release
|
|
||||||
// Song
|
|
||||||
std::vector<std::string> keywords;
|
|
||||||
boost::algorithm::split(keywords, text, boost::is_any_of(" "), boost::token_compress_on);
|
|
||||||
|
|
||||||
releaseSearch->search(SearchFilter::NameLikeMatch( {{
|
|
||||||
{ SearchFilter::Field::Artist, keywords },
|
|
||||||
{ SearchFilter::Field::Release, keywords }}}),
|
|
||||||
3);
|
|
||||||
|
|
||||||
artistSearch->search(SearchFilter::NameLikeMatch({{{SearchFilter::Field::Artist, keywords}}}),
|
|
||||||
3);
|
|
||||||
|
|
||||||
trackSearch->search(SearchFilter::NameLikeMatch({{{SearchFilter::Field::Track, keywords}}}),
|
|
||||||
3);
|
|
||||||
|
|
||||||
artistSearch->show();
|
|
||||||
releaseSearch->show();
|
|
||||||
trackSearch->show();
|
|
||||||
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
artistSearch->moreArtistsSelected().connect(std::bind([=]
|
_artistSearch->artistSelected().connect(std::bind([=] (Artist::id_type artistId)
|
||||||
{
|
{
|
||||||
releaseSearch->hide();
|
_artistSearch->hide();
|
||||||
trackSearch->hide();
|
_trackSearch->hide();
|
||||||
artistSearch->show();
|
_releaseSearch->show();
|
||||||
}));
|
_trackReleaseView->hide();
|
||||||
|
|
||||||
artistSearch->artistSelected().connect(std::bind([=] (Artist::id_type artistId)
|
_releaseSearch->search(SearchFilter::ById(SearchFilter::Field::Artist, artistId), 20);
|
||||||
{
|
|
||||||
artistSearch->hide();
|
|
||||||
trackSearch->hide();
|
|
||||||
releaseSearch->show();
|
|
||||||
|
|
||||||
releaseSearch->search(SearchFilter::ById(SearchFilter::Field::Artist, artistId), 20);
|
|
||||||
}, std::placeholders::_1));
|
}, std::placeholders::_1));
|
||||||
|
|
||||||
releaseSearch->moreReleasesSelected().connect(std::bind([=]
|
_releaseSearch->moreReleasesSelected().connect(std::bind([=]
|
||||||
{
|
{
|
||||||
artistSearch->hide();
|
_artistSearch->hide();
|
||||||
trackSearch->hide();
|
_trackSearch->hide();
|
||||||
releaseSearch->show();
|
_releaseSearch->show();
|
||||||
|
_trackReleaseView->hide();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
releaseSearch->releaseSelected().connect(std::bind([=] (Release::id_type releaseId)
|
_releaseSearch->releaseSelected().connect(std::bind([=] (Release::id_type releaseId)
|
||||||
{
|
{
|
||||||
artistSearch->hide();
|
_artistSearch->hide();
|
||||||
releaseSearch->hide();
|
_releaseSearch->hide();
|
||||||
trackSearch->show();
|
_trackSearch->hide();
|
||||||
|
_trackReleaseView->show();
|
||||||
|
|
||||||
trackSearch->search(SearchFilter::ById(SearchFilter::Field::Release, releaseId), 20);
|
_trackReleaseView->search(SearchFilter::ById(SearchFilter::Field::Release, releaseId), 40);
|
||||||
}, std::placeholders::_1));
|
}, std::placeholders::_1));
|
||||||
|
|
||||||
trackSearch->moreTracksSelected().connect(std::bind([=]
|
_trackSearch->moreSelected().connect(std::bind([=]
|
||||||
{
|
{
|
||||||
artistSearch->hide();
|
_artistSearch->hide();
|
||||||
releaseSearch->hide();
|
_releaseSearch->hide();
|
||||||
|
_trackSearch->show();
|
||||||
|
_trackReleaseView->hide();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
trackSearch->trackPlay().connect(std::bind([=] (Track::id_type id)
|
_trackSearch->trackPlay().connect(std::bind([=] (Track::id_type id)
|
||||||
{
|
{
|
||||||
LMS_LOG(UI, DEBUG) << "Playing track id " << id;
|
playTrack(audioPlayer, id);
|
||||||
// TODO reduce transaction scope here
|
}, std::placeholders::_1));
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
Track::pointer track = Track::getById(DboSession(), id);
|
_trackReleaseView->trackPlay().connect(std::bind([=] (Track::id_type id)
|
||||||
|
{
|
||||||
if (track)
|
playTrack(audioPlayer, id);
|
||||||
{
|
}, std::placeholders::_1));
|
||||||
Av::TranscodeParameters parameters;
|
|
||||||
|
|
||||||
// Determine the output format using the encoding of the player
|
|
||||||
Av::Encoding encoding;
|
|
||||||
switch(mediaPlayer->getEncoding())
|
|
||||||
{
|
|
||||||
case Wt::WMediaPlayer::MP3: encoding = Av::Encoding::MP3; break;
|
|
||||||
case Wt::WMediaPlayer::FLA: encoding = Av::Encoding::FLA; break;
|
|
||||||
case Wt::WMediaPlayer::OGA: encoding = Av::Encoding::OGA; break;
|
|
||||||
case Wt::WMediaPlayer::WEBMA: encoding = Av::Encoding::WEBMA; break;
|
|
||||||
default:
|
|
||||||
encoding = Av::Encoding::MP3;
|
|
||||||
}
|
|
||||||
parameters.setEncoding(encoding);
|
|
||||||
|
|
||||||
// TODO compute parameters using user's profile
|
|
||||||
parameters.setBitrate(Av::Stream::Type::Audio, 96000);
|
|
||||||
|
|
||||||
mediaPlayer->play(track.id(), parameters);
|
|
||||||
}
|
|
||||||
} , std::placeholders::_1));
|
|
||||||
|
|
||||||
// Initially, populate the widgets using an empty search
|
// Initially, populate the widgets using an empty search
|
||||||
{
|
{
|
||||||
artistSearch->search(SearchFilter(), 3);
|
_artistSearch->search(SearchFilter(), SEARCH_NB_ITEMS);
|
||||||
releaseSearch->search(SearchFilter(), 3);
|
_releaseSearch->search(SearchFilter(), SEARCH_NB_ITEMS);
|
||||||
trackSearch->search(SearchFilter(), 3);
|
_trackSearch->search(SearchFilter(), SEARCH_NB_ITEMS);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,11 @@
|
|||||||
|
|
||||||
#include "audio/Audio.hpp"
|
#include "audio/Audio.hpp"
|
||||||
|
|
||||||
|
#include "ArtistSearch.hpp"
|
||||||
|
#include "ReleaseSearch.hpp"
|
||||||
|
#include "TrackSearch.hpp"
|
||||||
|
#include "TrackReleaseView.hpp"
|
||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface {
|
||||||
namespace Mobile {
|
namespace Mobile {
|
||||||
|
|
||||||
@@ -32,10 +37,14 @@ class Audio : public UserInterface::Audio
|
|||||||
public:
|
public:
|
||||||
Audio(Wt::WContainerWidget *parent = 0);
|
Audio(Wt::WContainerWidget *parent = 0);
|
||||||
|
|
||||||
void search(std::string text) {}
|
void search(std::string text);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
|
ArtistSearch* _artistSearch;
|
||||||
|
ReleaseSearch* _releaseSearch;
|
||||||
|
TrackSearch* _trackSearch;
|
||||||
|
TrackReleaseView* _trackReleaseView;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Mobile
|
} // namespace Mobile
|
||||||
|
|||||||
@@ -1,110 +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 <Wt/WApplication>
|
|
||||||
#include <Wt/WEnvironment>
|
|
||||||
#include <Wt/WTemplate>
|
|
||||||
#include <Wt/WPushButton>
|
|
||||||
#include <Wt/WSlider>
|
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
|
|
||||||
#include "MobileAudioMediaPlayer.hpp"
|
|
||||||
|
|
||||||
#include "resource/AvConvTranscodeStreamResource.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
|
||||||
namespace Mobile {
|
|
||||||
|
|
||||||
Wt::WMediaPlayer::Encoding
|
|
||||||
AudioMediaPlayer::getBestEncoding()
|
|
||||||
{
|
|
||||||
// MP3 seems to be better supported everywhere
|
|
||||||
return Wt::WMediaPlayer::MP3;
|
|
||||||
}
|
|
||||||
|
|
||||||
AudioMediaPlayer::AudioMediaPlayer(Wt::WMediaPlayer::Encoding encoding, Wt::WContainerWidget *parent)
|
|
||||||
:
|
|
||||||
Wt::WContainerWidget(parent),
|
|
||||||
_player(nullptr),
|
|
||||||
_encoding(encoding)
|
|
||||||
{
|
|
||||||
Wt::WTemplate* container = new Wt::WTemplate(this);
|
|
||||||
container->setTemplateText(Wt::WString::tr("mobile-audio-player"));
|
|
||||||
|
|
||||||
Wt::WPushButton *play = new Wt::WPushButton("Play");
|
|
||||||
play->setStyleClass("btn-sm");
|
|
||||||
play->setWidth(52);
|
|
||||||
container->bindWidget("play", play);
|
|
||||||
|
|
||||||
Wt::WPushButton *pause = new Wt::WPushButton("Pause");
|
|
||||||
pause->setStyleClass("btn-sm");
|
|
||||||
pause->setWidth(52);
|
|
||||||
container->bindWidget("pause", pause);
|
|
||||||
|
|
||||||
_track = new Wt::WText("", Wt::PlainText);
|
|
||||||
_track->setStyleClass("mobile-track");
|
|
||||||
container->bindWidget("track", _track);
|
|
||||||
|
|
||||||
_artistRelease = new Wt::WText("", Wt::PlainText);
|
|
||||||
_artistRelease->setStyleClass("mobile-artist");
|
|
||||||
container->bindWidget("artist", _artistRelease);
|
|
||||||
|
|
||||||
_cover = new Wt::WImage();
|
|
||||||
container->bindWidget("cover", _cover);
|
|
||||||
_cover->setStyleClass("mobile-audio-player-cover");
|
|
||||||
|
|
||||||
|
|
||||||
_player = new Wt::WMediaPlayer(Wt::WMediaPlayer::Audio, this);
|
|
||||||
_player->addSource( _encoding, "" );
|
|
||||||
_player->setControlsWidget( 0 );
|
|
||||||
_player->setButton(Wt::WMediaPlayer::Play, play);
|
|
||||||
_player->setButton(Wt::WMediaPlayer::Pause, pause);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
AudioMediaPlayer::play(Database::Track::id_type trackId, Av::TranscodeParameters parameters)
|
|
||||||
{
|
|
||||||
boost::filesystem::path path;
|
|
||||||
|
|
||||||
{
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
|
||||||
|
|
||||||
Database::Track::pointer track = Database::Track::getById(DboSession(), trackId);
|
|
||||||
|
|
||||||
_track->setText( Wt::WString::fromUTF8(track->getName() ));
|
|
||||||
_artistRelease->setText( Wt::WString::fromUTF8(track->getArtist()->getName()) );
|
|
||||||
path = track->getPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME memleak here
|
|
||||||
AvConvTranscodeStreamResource *resource = new AvConvTranscodeStreamResource( path, parameters, this );
|
|
||||||
|
|
||||||
_player->clearSources();
|
|
||||||
_player->addSource( _encoding, Wt::WLink(resource));
|
|
||||||
|
|
||||||
_player->play();
|
|
||||||
|
|
||||||
_cover->setImageLink( Wt::WLink (LmsApplication::instance()->getCoverResource()->getTrackUrl(trackId, 48)));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace UserInterface
|
|
||||||
} // namespace Mobile
|
|
||||||
@@ -40,6 +40,11 @@ _resCount(0)
|
|||||||
title->setTemplateText(Wt::WString::tr("mobile-search-title"));
|
title->setTemplateText(Wt::WString::tr("mobile-search-title"));
|
||||||
|
|
||||||
title->bindWidget("text", new Wt::WText("Releases", Wt::PlainText));
|
title->bindWidget("text", new Wt::WText("Releases", Wt::PlainText));
|
||||||
|
|
||||||
|
Wt::WTemplate* releaseWrapper = new Wt::WTemplate(this);
|
||||||
|
releaseWrapper->setTemplateText(Wt::WString::tr("wa-release-wrapper"));
|
||||||
|
_contents = new Wt::WContainerWidget();
|
||||||
|
releaseWrapper->bindWidget("contents", _contents );
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -58,9 +63,23 @@ ReleaseSearch::search(Database::SearchFilter filter, size_t max)
|
|||||||
addResults(filter, max);
|
addResults(filter, max);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Wt::WString
|
||||||
|
getArtistFromRelease(Release::pointer release)
|
||||||
|
{
|
||||||
|
auto artists = Artist::getByFilter(DboSession(),
|
||||||
|
SearchFilter::ById(SearchFilter::Field::Release, release.id()), -1, 2);
|
||||||
|
|
||||||
|
if (artists.size() > 1)
|
||||||
|
return Wt::WString::fromUTF8("Various artists", Wt::PlainText);
|
||||||
|
else
|
||||||
|
return Wt::WString::fromUTF8(artists.front()->getName(), Wt::PlainText);
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
ReleaseSearch::addResults(Database::SearchFilter filter, size_t nb)
|
ReleaseSearch::addResults(Database::SearchFilter filter, size_t nb)
|
||||||
{
|
{
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
Wt::Dbo::Transaction transaction(DboSession());
|
||||||
|
|
||||||
std::vector<Release::pointer> releases = Release::getByFilter(DboSession(), filter, _resCount, nb + 1);
|
std::vector<Release::pointer> releases = Release::getByFilter(DboSession(), filter, _resCount, nb + 1);
|
||||||
@@ -77,14 +96,17 @@ ReleaseSearch::addResults(Database::SearchFilter filter, size_t nb)
|
|||||||
for (Release::pointer release : releases)
|
for (Release::pointer release : releases)
|
||||||
{
|
{
|
||||||
Wt::WTemplate* releaseWidget = new Wt::WTemplate(this);
|
Wt::WTemplate* releaseWidget = new Wt::WTemplate(this);
|
||||||
releaseWidget->setTemplateText(Wt::WString::tr("mobile-release-res"));
|
releaseWidget->setTemplateText(Wt::WString::tr("wa-release-res"));
|
||||||
|
|
||||||
Wt::WImage *cover = new Wt::WImage();
|
Wt::WImage *cover = new Wt::WImage();
|
||||||
cover->setStyleClass("center-block");
|
cover->setStyleClass("center-block");
|
||||||
cover->setImageLink( Wt::WLink( LmsApplication::instance()->getCoverResource()->getReleaseUrl(release.id(), 56)));
|
cover->setImageLink( Wt::WLink( LmsApplication::instance()->getCoverResource()->getReleaseUrl(release.id(), 512)));
|
||||||
releaseWidget->bindWidget("cover", cover);
|
cover->setStyleClass("release_res_shadow release_img-responsive"); // TODO move?
|
||||||
|
|
||||||
|
releaseWidget->bindWidget("cover", cover);
|
||||||
releaseWidget->bindWidget("name", new Wt::WText(Wt::WString::fromUTF8(release->getName()), Wt::PlainText));
|
releaseWidget->bindWidget("name", new Wt::WText(Wt::WString::fromUTF8(release->getName()), Wt::PlainText));
|
||||||
|
releaseWidget->bindString("release_name", Wt::WString::fromUTF8(release->getName()), Wt::PlainText);
|
||||||
|
releaseWidget->bindString("artist", getArtistFromRelease(release));
|
||||||
|
|
||||||
releaseWidget->clicked().connect(std::bind([=] {
|
releaseWidget->clicked().connect(std::bind([=] {
|
||||||
_sigReleaseSelected(release.id());
|
_sigReleaseSelected(release.id());
|
||||||
|
|||||||
@@ -20,8 +20,6 @@
|
|||||||
#ifndef UI_MOBILE_RELEASE_SEARCH_HPP
|
#ifndef UI_MOBILE_RELEASE_SEARCH_HPP
|
||||||
#define UI_MOBILE_RELEASE_SEARCH_HPP
|
#define UI_MOBILE_RELEASE_SEARCH_HPP
|
||||||
|
|
||||||
#include <boost/algorithm/string/split.hpp>
|
|
||||||
|
|
||||||
#include <Wt/WContainerWidget>
|
#include <Wt/WContainerWidget>
|
||||||
#include <Wt/WSignal>
|
#include <Wt/WSignal>
|
||||||
|
|
||||||
@@ -48,6 +46,7 @@ class ReleaseSearch : public Wt::WContainerWidget
|
|||||||
void clear(void);
|
void clear(void);
|
||||||
void addResults(Database::SearchFilter filter, size_t nb);
|
void addResults(Database::SearchFilter filter, size_t nb);
|
||||||
|
|
||||||
|
Wt::WContainerWidget* _contents;
|
||||||
std::size_t _resCount;
|
std::size_t _resCount;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
/*
|
||||||
|
* 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 <Wt/WText>
|
||||||
|
#include <Wt/WImage>
|
||||||
|
#include <Wt/WPushButton>
|
||||||
|
#include <Wt/WTemplate>
|
||||||
|
|
||||||
|
#include "logger/Logger.hpp"
|
||||||
|
#include "utils/Utils.hpp"
|
||||||
|
#include "LmsApplication.hpp"
|
||||||
|
|
||||||
|
#include "TrackReleaseView.hpp"
|
||||||
|
|
||||||
|
namespace UserInterface {
|
||||||
|
namespace Mobile {
|
||||||
|
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
TrackReleaseView::TrackReleaseView(Wt::WContainerWidget *parent)
|
||||||
|
: Wt::WContainerWidget(parent)
|
||||||
|
{
|
||||||
|
Wt::WTemplate* wrapper = new Wt::WTemplate(this);
|
||||||
|
wrapper->setTemplateText(Wt::WString::tr("wa-trackview-wrapper"));
|
||||||
|
|
||||||
|
Wt::WTemplate* title = new Wt::WTemplate(this);
|
||||||
|
wrapper->bindWidget("title", title);
|
||||||
|
|
||||||
|
title->setTemplateText(Wt::WString::tr("mobile-search-title"));
|
||||||
|
title->bindString("text", "Releases", Wt::PlainText);
|
||||||
|
|
||||||
|
_releaseContainer = new Wt::WContainerWidget();
|
||||||
|
wrapper->bindWidget("release-container", _releaseContainer);
|
||||||
|
|
||||||
|
_showMore = new Wt::WTemplate();
|
||||||
|
wrapper->bindWidget("show-more", _showMore);
|
||||||
|
|
||||||
|
_showMore->setTemplateText(Wt::WString::tr("mobile-search-more"));
|
||||||
|
_showMore->bindString("text", "Tap to show more results...");
|
||||||
|
_showMore->hide();
|
||||||
|
_showMore->clicked().connect(std::bind([=] {
|
||||||
|
_sigMoreTracksSelected();
|
||||||
|
addResults(20);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TrackReleaseView::clear()
|
||||||
|
{
|
||||||
|
// Flush the release container
|
||||||
|
_releaseContainer->clear();
|
||||||
|
|
||||||
|
// Flush the current context
|
||||||
|
_currentTrackContainer = nullptr;
|
||||||
|
_nbTracks = 0;
|
||||||
|
_showMore->hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TrackReleaseView::search(SearchFilter filter, size_t nb)
|
||||||
|
{
|
||||||
|
_filter = filter;
|
||||||
|
|
||||||
|
clear();
|
||||||
|
addResults(nb);
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
std::vector<Track::pointer >
|
||||||
|
getTracks(SearchFilter filter, size_t offset, size_t nb, bool &moreResults)
|
||||||
|
{
|
||||||
|
std::vector<Track::pointer > tracks = Track::getByFilter(DboSession(), filter, offset, nb + 1);
|
||||||
|
|
||||||
|
if (tracks.size() == nb + 1)
|
||||||
|
{
|
||||||
|
moreResults = true;
|
||||||
|
tracks.pop_back();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
moreResults = false;
|
||||||
|
|
||||||
|
return tracks;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Wt::WString
|
||||||
|
getArtistNameFromRelease(Release::pointer release)
|
||||||
|
{
|
||||||
|
auto artists = Artist::getByFilter(DboSession(),
|
||||||
|
SearchFilter::ById(SearchFilter::Field::Release, release.id()), -1, 2);
|
||||||
|
|
||||||
|
if (artists.size() > 1)
|
||||||
|
return Wt::WString::fromUTF8("Various artists", Wt::PlainText);
|
||||||
|
else
|
||||||
|
return Wt::WString::fromUTF8(artists.front()->getName(), Wt::PlainText);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TrackReleaseView::addResults(size_t nb)
|
||||||
|
{
|
||||||
|
Wt::Dbo::Transaction transaction(DboSession());
|
||||||
|
|
||||||
|
bool moreResults;
|
||||||
|
std::vector<Track::pointer > tracks = getTracks(_filter, _nbTracks, nb, moreResults);
|
||||||
|
|
||||||
|
for (Track::pointer track : tracks)
|
||||||
|
{
|
||||||
|
// First check if we need to create a new track container
|
||||||
|
|
||||||
|
// New container if it is the first one or if the release has changed
|
||||||
|
if (!_currentTrackContainer
|
||||||
|
|| _currentReleaseId != track->getRelease().id())
|
||||||
|
{
|
||||||
|
Release::pointer release = track->getRelease();
|
||||||
|
|
||||||
|
Wt::WTemplate *releaseContainer = new Wt::WTemplate(_releaseContainer);
|
||||||
|
releaseContainer->setTemplateText(Wt::WString::tr("wa-trackview-release-container"));
|
||||||
|
_currentReleaseId = release.id();
|
||||||
|
|
||||||
|
Wt::WImage *cover = new Wt::WImage();
|
||||||
|
cover->setStyleClass ("center-block img-responsive"); // TODO move to CSS?
|
||||||
|
cover->setImageLink(Wt::WLink (LmsApplication::instance()->getCoverResource()->getReleaseUrl(release.id(), 512)));
|
||||||
|
|
||||||
|
releaseContainer->bindWidget("cover", cover);
|
||||||
|
releaseContainer->bindString("artist-name", getArtistNameFromRelease(release), Wt::PlainText);
|
||||||
|
releaseContainer->bindString("release-name", release->getName(), Wt::PlainText);
|
||||||
|
int year = release->getReleaseYear();
|
||||||
|
if (year > 0)
|
||||||
|
{
|
||||||
|
releaseContainer->setCondition("if-has-year", true);
|
||||||
|
releaseContainer->bindInt("year", year);
|
||||||
|
|
||||||
|
int originalYear = release->getReleaseYear(true);
|
||||||
|
if (originalYear > 0 && originalYear != year)
|
||||||
|
{
|
||||||
|
releaseContainer->setCondition("if-has-orig-year", true);
|
||||||
|
releaseContainer->bindInt("orig-year", originalYear);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentTrackContainer = new Wt::WContainerWidget();
|
||||||
|
releaseContainer->bindWidget("track-container", _currentTrackContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(_currentTrackContainer != nullptr);
|
||||||
|
|
||||||
|
Wt::WTemplate* trackRes = new Wt::WTemplate(_currentTrackContainer);
|
||||||
|
trackRes->setTemplateText(Wt::WString::tr("wa-trackview-track"));
|
||||||
|
|
||||||
|
if (track->getTrackNumber() > 0 && !track->getRelease()->isNone())
|
||||||
|
{
|
||||||
|
trackRes->setCondition("if-has-track-num", true);
|
||||||
|
trackRes->bindInt("track-num", track->getTrackNumber());
|
||||||
|
|
||||||
|
if (track->getDiscNumber() > 0 && track->getTotalDiscNumber() > 1)
|
||||||
|
{ trackRes->setCondition("if-has-disc-num", true);
|
||||||
|
trackRes->bindInt("disc-num", track->getDiscNumber());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trackRes->bindString("track-name", Wt::WString::fromUTF8(track->getName()), Wt::PlainText);
|
||||||
|
// TODO, display artist name for compilation releases?
|
||||||
|
|
||||||
|
// TODO handle large duration (> 1 hour)
|
||||||
|
trackRes->bindString("time", durationToString(track->getDuration(), "%M:%S"), Wt::PlainText);
|
||||||
|
|
||||||
|
Wt::WText *playBtn = new Wt::WText("Play", Wt::PlainText);
|
||||||
|
playBtn->setStyleClass("center-block"); // TODO move to CSS?
|
||||||
|
playBtn->clicked().connect(std::bind([=] {
|
||||||
|
_sigTrackPlay.emit(track.id());
|
||||||
|
}));
|
||||||
|
|
||||||
|
trackRes->bindWidget("btn", playBtn);
|
||||||
|
_nbTracks++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moreResults)
|
||||||
|
_showMore->show();
|
||||||
|
else
|
||||||
|
_showMore->hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Mobile
|
||||||
|
} // namespace UserInterface
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* 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 UI_MOBILE_TRACK_SEARCH_HPP
|
||||||
|
#define UI_MOBILE_TRACK_SEARCH_HPP
|
||||||
|
|
||||||
|
#include <Wt/WContainerWidget>
|
||||||
|
#include <Wt/WSignal>
|
||||||
|
|
||||||
|
#include "resource/CoverResource.hpp"
|
||||||
|
|
||||||
|
namespace UserInterface {
|
||||||
|
namespace Mobile {
|
||||||
|
|
||||||
|
class TrackReleaseView : public Wt::WContainerWidget
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
|
||||||
|
TrackReleaseView(Wt::WContainerWidget *parent = 0);
|
||||||
|
|
||||||
|
void search(Database::SearchFilter filter, size_t nb);
|
||||||
|
|
||||||
|
// Slots
|
||||||
|
Wt::Signal<Database::Track::id_type>& trackPlay() { return _sigTrackPlay;}
|
||||||
|
Wt::Signal<void>& moreTracksSelected() { return _sigMoreTracksSelected;}
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
Wt::Signal<Database::Track::id_type> _sigTrackPlay;
|
||||||
|
Wt::Signal<void> _sigMoreTracksSelected;
|
||||||
|
|
||||||
|
void clear(void);
|
||||||
|
void addResults(size_t nb);
|
||||||
|
|
||||||
|
Wt::WTemplate* _showMore;
|
||||||
|
|
||||||
|
Database::SearchFilter _filter;
|
||||||
|
std::size_t _nbTracks;
|
||||||
|
|
||||||
|
// Main container that holds the releases
|
||||||
|
Wt::WContainerWidget* _releaseContainer;
|
||||||
|
|
||||||
|
// Used to add more results on the fly
|
||||||
|
Database::Release::id_type _currentReleaseId;
|
||||||
|
Wt::WContainerWidget* _currentTrackContainer;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Mobile
|
||||||
|
} // namespace UserInterface
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
#include <Wt/WTemplate>
|
#include <Wt/WTemplate>
|
||||||
|
|
||||||
#include "logger/Logger.hpp"
|
#include "logger/Logger.hpp"
|
||||||
|
#include "utils/Utils.hpp"
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
|
|
||||||
#include "TrackSearch.hpp"
|
#include "TrackSearch.hpp"
|
||||||
@@ -33,101 +34,103 @@ namespace Mobile {
|
|||||||
using namespace Database;
|
using namespace Database;
|
||||||
|
|
||||||
TrackSearch::TrackSearch(Wt::WContainerWidget *parent)
|
TrackSearch::TrackSearch(Wt::WContainerWidget *parent)
|
||||||
: Wt::WContainerWidget(parent),
|
: Wt::WContainerWidget(parent)
|
||||||
_resCount(0)
|
|
||||||
{
|
{
|
||||||
|
Wt::WTemplate* wrapper = new Wt::WTemplate(this);
|
||||||
|
wrapper->setTemplateText(Wt::WString::tr("wa-track-wrapper"));
|
||||||
|
|
||||||
Wt::WTemplate* title = new Wt::WTemplate(this);
|
Wt::WTemplate* title = new Wt::WTemplate(this);
|
||||||
|
wrapper->bindWidget("title", title);
|
||||||
|
|
||||||
title->setTemplateText(Wt::WString::tr("mobile-search-title"));
|
title->setTemplateText(Wt::WString::tr("mobile-search-title"));
|
||||||
|
title->bindString("text", "Tracks", Wt::PlainText);
|
||||||
|
|
||||||
title->bindWidget("text", new Wt::WText("Tracks", Wt::PlainText));
|
_container = new Wt::WContainerWidget();
|
||||||
|
wrapper->bindWidget("track-container", _container);
|
||||||
|
|
||||||
|
_showMore = new Wt::WTemplate();
|
||||||
|
wrapper->bindWidget("show-more", _showMore);
|
||||||
|
|
||||||
|
_showMore->setTemplateText(Wt::WString::tr("mobile-search-more"));
|
||||||
|
_showMore->bindString("text", "Tap to show more results...");
|
||||||
|
_showMore->hide();
|
||||||
|
_showMore->clicked().connect(std::bind([=] {
|
||||||
|
_sigMoreSelected();
|
||||||
|
addResults(20);
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
TrackSearch::clear()
|
TrackSearch::clear()
|
||||||
{
|
{
|
||||||
while (count() > 1)
|
// Flush the release container
|
||||||
removeWidget(this->widget(1));
|
_container->clear();
|
||||||
|
|
||||||
_resCount = 0;
|
_nbTracks = 0;
|
||||||
|
_showMore->hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
TrackSearch::search(Database::SearchFilter filter, size_t max)
|
TrackSearch::search(SearchFilter filter, size_t nb)
|
||||||
{
|
{
|
||||||
|
_filter = filter;
|
||||||
|
|
||||||
clear();
|
clear();
|
||||||
addResults(filter, max);
|
addResults(nb);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
static
|
||||||
TrackSearch::addResults(Database::SearchFilter filter, size_t nb)
|
std::vector<Track::pointer >
|
||||||
|
getTracks(SearchFilter filter, size_t offset, size_t nb, bool &moreResults)
|
||||||
{
|
{
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
std::vector<Track::pointer > tracks = Track::getByFilter(DboSession(), filter, offset, nb + 1);
|
||||||
|
|
||||||
std::vector<Track::pointer > tracks = Track::getByFilter(DboSession(), filter, _resCount, nb + 1);
|
|
||||||
|
|
||||||
bool expectMoreResults;
|
|
||||||
if (tracks.size() == nb + 1)
|
if (tracks.size() == nb + 1)
|
||||||
{
|
{
|
||||||
expectMoreResults = true;
|
moreResults = true;
|
||||||
tracks.pop_back();
|
tracks.pop_back();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
expectMoreResults = false;
|
moreResults = false;
|
||||||
|
|
||||||
|
return tracks;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TrackSearch::addResults(size_t nb)
|
||||||
|
{
|
||||||
|
Wt::Dbo::Transaction transaction(DboSession());
|
||||||
|
|
||||||
|
bool moreResults;
|
||||||
|
std::vector<Track::pointer > tracks = getTracks(_filter, _nbTracks, nb, moreResults);
|
||||||
|
|
||||||
for (Track::pointer track : tracks)
|
for (Track::pointer track : tracks)
|
||||||
{
|
{
|
||||||
Wt::WTemplate* trackWidget = new Wt::WTemplate(this);
|
Wt::WTemplate* trackRes = new Wt::WTemplate(_container);
|
||||||
trackWidget->setTemplateText(Wt::WString::tr("mobile-track-res"));
|
trackRes->setTemplateText(Wt::WString::tr("wa-track"));
|
||||||
|
|
||||||
Wt::WImage *cover = new Wt::WImage();
|
Wt::WImage *cover = new Wt::WImage();
|
||||||
cover->setStyleClass("center-block");
|
cover->setStyleClass ("center-block img-responsive");
|
||||||
cover->setImageLink( Wt::WLink (LmsApplication::instance()->getCoverResource()->getTrackUrl(track.id(), 56)) );
|
cover->setImageLink(Wt::WLink(LmsApplication::instance()->getCoverResource()->getTrackUrl(track.id(), 128)));
|
||||||
trackWidget->bindWidget("cover", cover);
|
trackRes->bindWidget("cover", cover);
|
||||||
|
|
||||||
// Track Name (bold)
|
trackRes->bindString("track-name", Wt::WString::fromUTF8(track->getName()), Wt::PlainText);
|
||||||
// Artist - Album (italic)
|
trackRes->bindString("artist-name", Wt::WString::fromUTF8(track->getArtist()->getName()), Wt::PlainText);
|
||||||
Wt::WContainerWidget *container = new Wt::WContainerWidget();
|
|
||||||
Wt::WText *title = new Wt::WText(Wt::WString::fromUTF8(track->getName()), Wt::PlainText);
|
|
||||||
title->setStyleClass("mobile-track");
|
|
||||||
container->addWidget(title);
|
|
||||||
|
|
||||||
if (!track->getArtist()->getName().empty()
|
Wt::WText *playBtn = new Wt::WText("Play", Wt::PlainText);
|
||||||
|| !track->getRelease()->getName().empty())
|
playBtn->setStyleClass("center-block"); // TODO move to CSS?
|
||||||
{
|
|
||||||
title->setInline(false);
|
|
||||||
Wt::WText *artistRelease = new Wt::WText(Wt::WString::fromUTF8(track->getArtist()->getName() + " - " + track->getRelease()->getName()), Wt::PlainText);
|
|
||||||
artistRelease->setStyleClass("mobile-artist");
|
|
||||||
container->addWidget(artistRelease);
|
|
||||||
}
|
|
||||||
trackWidget->bindWidget("name", container);
|
|
||||||
|
|
||||||
Wt::WPushButton *playBtn = new Wt::WPushButton("Play");
|
|
||||||
playBtn->setStyleClass("btn-primary center-block");
|
|
||||||
playBtn->clicked().connect(std::bind([=] {
|
playBtn->clicked().connect(std::bind([=] {
|
||||||
_sigTrackPlay.emit(track.id());
|
_sigTrackPlay.emit(track.id());
|
||||||
}));
|
}));
|
||||||
|
|
||||||
trackWidget->bindWidget("btn", playBtn);
|
trackRes->bindWidget("btn", playBtn);
|
||||||
|
_nbTracks++;
|
||||||
}
|
}
|
||||||
|
|
||||||
_resCount += tracks.size();
|
if (moreResults)
|
||||||
|
_showMore->show();
|
||||||
if (expectMoreResults)
|
else
|
||||||
{
|
_showMore->hide();
|
||||||
Wt::WTemplate* moreRes = new Wt::WTemplate(this);
|
|
||||||
moreRes->setTemplateText(Wt::WString::tr("mobile-search-more"));
|
|
||||||
|
|
||||||
moreRes->bindWidget("text", new Wt::WText("Tap to show more results..."));
|
|
||||||
|
|
||||||
moreRes->clicked().connect(std::bind([=] {
|
|
||||||
_sigMoreTracksSelected();
|
|
||||||
removeWidget(moreRes);
|
|
||||||
|
|
||||||
addResults(filter, 20);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Mobile
|
} // namespace Mobile
|
||||||
|
|||||||
@@ -17,16 +17,11 @@
|
|||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef UI_MOBILE_TRACK_SEARCH_HPP
|
#pragma once
|
||||||
#define UI_MOBILE_TRACK_SEARCH_HPP
|
|
||||||
|
|
||||||
#include <boost/algorithm/string/split.hpp>
|
|
||||||
|
|
||||||
#include <Wt/WContainerWidget>
|
#include <Wt/WContainerWidget>
|
||||||
#include <Wt/WSignal>
|
#include <Wt/WSignal>
|
||||||
|
|
||||||
#include "resource/CoverResource.hpp"
|
|
||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface {
|
||||||
namespace Mobile {
|
namespace Mobile {
|
||||||
|
|
||||||
@@ -40,20 +35,23 @@ class TrackSearch : public Wt::WContainerWidget
|
|||||||
|
|
||||||
// Slots
|
// Slots
|
||||||
Wt::Signal<Database::Track::id_type>& trackPlay() { return _sigTrackPlay;}
|
Wt::Signal<Database::Track::id_type>& trackPlay() { return _sigTrackPlay;}
|
||||||
Wt::Signal<void>& moreTracksSelected() { return _sigMoreTracksSelected;}
|
Wt::Signal<void>& moreSelected() { return _sigMoreSelected;}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
Wt::Signal<Database::Track::id_type> _sigTrackPlay;
|
Wt::Signal<Database::Track::id_type> _sigTrackPlay;
|
||||||
Wt::Signal<void> _sigMoreTracksSelected;
|
Wt::Signal<void> _sigMoreSelected;
|
||||||
|
|
||||||
void clear(void);
|
void clear(void);
|
||||||
void addResults(Database::SearchFilter filter, size_t nb);
|
void addResults(size_t nb);
|
||||||
|
|
||||||
std::size_t _resCount;
|
Wt::WTemplate* _showMore;
|
||||||
|
|
||||||
|
Database::SearchFilter _filter;
|
||||||
|
std::size_t _nbTracks;
|
||||||
|
Wt::WContainerWidget* _container;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Mobile
|
} // namespace Mobile
|
||||||
} // namespace UserInterface
|
} // namespace UserInterface
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* 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 <Wt/WConfig.h>
|
||||||
|
|
||||||
|
#if WT_VERSION < 0X03030500
|
||||||
|
#include <Wt/WTemplate>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "InputRange.hpp"
|
||||||
|
|
||||||
|
InputRange::InputRange(Wt::WContainerWidget *parent)
|
||||||
|
: Wt::WWebWidget(parent)
|
||||||
|
{
|
||||||
|
//TODO woraround for older Wt versions
|
||||||
|
#if WT_VERSION >= 0X03030500
|
||||||
|
setHtmlTagName("input");
|
||||||
|
setAttributeValue("type", "range");
|
||||||
|
#else
|
||||||
|
Wt::WTemplate *t = new Wt::WTemplate("<input type=\"range\"></input>", parent);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::string
|
||||||
|
InputRange::jsRef(void) const
|
||||||
|
{
|
||||||
|
#if WT_VERSION >= 0X03030500
|
||||||
|
return Wt::WWebWidget::jsRef();
|
||||||
|
#else
|
||||||
|
return this->jsRef() + ".getElementsByTagName(\"input\")[0]";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Wt/WContainerWidget>
|
||||||
|
#include <Wt/WWebWidget>
|
||||||
|
|
||||||
|
class InputRange : public Wt::WWebWidget
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
InputRange(Wt::WContainerWidget *parent = 0);
|
||||||
|
|
||||||
|
Wt::DomElementType domElementType() const
|
||||||
|
{
|
||||||
|
return Wt::DomElement_INPUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string jsRef() const;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ class AvConvTranscodeStreamResource : public Wt::WResource
|
|||||||
boost::filesystem::path _filePath;
|
boost::filesystem::path _filePath;
|
||||||
Av::TranscodeParameters _parameters;
|
Av::TranscodeParameters _parameters;
|
||||||
|
|
||||||
static const std::size_t _bufferSize = 8192*16;
|
static const std::size_t _bufferSize = 8192;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace UserInterface
|
} // namespace UserInterface
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
#include <Wt/Http/Response>
|
#include <Wt/Http/Response>
|
||||||
|
|
||||||
#include "logger/Logger.hpp"
|
#include "logger/Logger.hpp"
|
||||||
|
#include "LmsApplication.hpp"
|
||||||
|
|
||||||
#include "cover/CoverArtGrabber.hpp"
|
#include "cover/CoverArtGrabber.hpp"
|
||||||
|
|
||||||
@@ -36,7 +37,7 @@ _db(db)
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
CoverResource:: ~CoverResource()
|
CoverResource::~CoverResource()
|
||||||
{
|
{
|
||||||
beingDeleted();
|
beingDeleted();
|
||||||
}
|
}
|
||||||
@@ -121,7 +122,7 @@ CoverResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons
|
|||||||
|
|
||||||
{
|
{
|
||||||
// transactions are not thread safe
|
// transactions are not thread safe
|
||||||
std::unique_lock<std::mutex> lock(_mutex);
|
Wt::WApplication::UpdateLock lock(LmsApplication::instance());
|
||||||
|
|
||||||
Wt::Dbo::Transaction transaction(_db.getSession());
|
Wt::Dbo::Transaction transaction(_db.getSession());
|
||||||
|
|
||||||
@@ -147,8 +148,9 @@ CoverResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons
|
|||||||
else if (releaseIdStr)
|
else if (releaseIdStr)
|
||||||
{
|
{
|
||||||
Database::Release::id_type releaseId = std::stol(*releaseIdStr);
|
Database::Release::id_type releaseId = std::stol(*releaseIdStr);
|
||||||
|
|
||||||
// transactions are not thread safe
|
// transactions are not thread safe
|
||||||
std::unique_lock<std::mutex> lock(_mutex);
|
Wt::WApplication::UpdateLock lock(LmsApplication::instance());
|
||||||
Wt::Dbo::Transaction transaction(_db.getSession());
|
Wt::Dbo::Transaction transaction(_db.getSession());
|
||||||
|
|
||||||
covers = CoverArt::Grabber::instance().getFromRelease(_db.getSession(), releaseId);
|
covers = CoverArt::Grabber::instance().getFromRelease(_db.getSession(), releaseId);
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class CoverResource : public Wt::WResource
|
|||||||
|
|
||||||
std::string getReleaseUrl(Database::Release::id_type releaseId, size_t size) const;
|
std::string getReleaseUrl(Database::Release::id_type releaseId, size_t size) const;
|
||||||
std::string getTrackUrl(Database::Track::id_type trackId, size_t size) const;
|
std::string getTrackUrl(Database::Track::id_type trackId, size_t size) const;
|
||||||
|
std::string getArtistUrl(Database::Artist::id_type artistId, size_t size) const;
|
||||||
std::string getUnknownTrackUrl(size_t size) const;
|
std::string getUnknownTrackUrl(size_t size) const;
|
||||||
|
|
||||||
void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response);
|
void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response);
|
||||||
@@ -50,6 +51,7 @@ class CoverResource : public Wt::WResource
|
|||||||
Image::Image getDefaultCover(std::size_t size);
|
Image::Image getDefaultCover(std::size_t size);
|
||||||
void putCover(Wt::Http::Response& response, Image::Image image);
|
void putCover(Wt::Http::Response& response, Image::Image image);
|
||||||
|
|
||||||
|
// Used to protect transactions since they are not thread safe
|
||||||
std::mutex _mutex;
|
std::mutex _mutex;
|
||||||
Database::Handler& _db;
|
Database::Handler& _db;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
/*
|
||||||
|
* 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 <Wt/Http/Response>
|
||||||
|
|
||||||
|
#include "logger/Logger.hpp"
|
||||||
|
#include "LmsApplication.hpp"
|
||||||
|
|
||||||
|
#include "TranscodeResource.hpp"
|
||||||
|
|
||||||
|
namespace UserInterface {
|
||||||
|
|
||||||
|
TranscodeResource::TranscodeResource(Database::Handler& db, Wt::WObject *parent)
|
||||||
|
: Wt::WResource(parent),
|
||||||
|
_db(db)
|
||||||
|
{
|
||||||
|
LMS_LOG(UI, DEBUG) << "CONSTRUCTING RESOURCE";
|
||||||
|
}
|
||||||
|
|
||||||
|
TranscodeResource:: ~TranscodeResource()
|
||||||
|
{
|
||||||
|
LMS_LOG(UI, DEBUG) << "DESTRUCTING RESOURCE";
|
||||||
|
beingDeleted();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string
|
||||||
|
TranscodeResource::getUrl(Database::Track::id_type trackId, Av::Encoding encoding, std::size_t offset, std::vector<std::size_t> streamIds) const
|
||||||
|
{
|
||||||
|
std::string res = url()+ "&trackid=" + std::to_string(trackId) + "&encoding=" + std::to_string(Av::encoding_to_int(encoding));
|
||||||
|
if (!streamIds.empty())
|
||||||
|
{
|
||||||
|
for (std::size_t streamId : streamIds)
|
||||||
|
res += "&stream=" + std::to_string(streamId);
|
||||||
|
}
|
||||||
|
res += "&offset=" + std::to_string(offset);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TranscodeResource::handleRequest(const Wt::Http::Request& request,
|
||||||
|
Wt::Http::Response& response)
|
||||||
|
{
|
||||||
|
// Retrieve parameters
|
||||||
|
const std::string *trackIdStr = request.getParameter("trackid");
|
||||||
|
const std::string *offsetStr = request.getParameter("offset");
|
||||||
|
const std::string *encodingStr = request.getParameter("encoding");
|
||||||
|
std::vector<std::string> streams = request.getParameterValues ("stream");
|
||||||
|
|
||||||
|
LMS_LOG(UI, DEBUG) << "Handling new request...";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
std::shared_ptr<Av::Transcoder> transcoder;
|
||||||
|
|
||||||
|
// First, see if this request is for a continuation
|
||||||
|
Wt::Http::ResponseContinuation *continuation = request.continuation();
|
||||||
|
if (continuation)
|
||||||
|
{
|
||||||
|
LMS_LOG(UI, DEBUG) << "Continuation! " << continuation ;
|
||||||
|
transcoder = boost::any_cast<std::shared_ptr<Av::Transcoder> >(continuation->data());
|
||||||
|
if (!transcoder)
|
||||||
|
{
|
||||||
|
LMS_LOG(UI, ERROR) << "No transcoder set -> abort!";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LMS_LOG(UI, DEBUG) << "No continuation yet";
|
||||||
|
|
||||||
|
if (!trackIdStr
|
||||||
|
|| !offsetStr
|
||||||
|
|| !encodingStr)
|
||||||
|
{
|
||||||
|
LMS_LOG(UI, ERROR) << "Missing transcode parameter";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Database::Track::id_type trackId = std::stol(*trackIdStr);
|
||||||
|
|
||||||
|
// transactions are not thread safe
|
||||||
|
{
|
||||||
|
Wt::WApplication::UpdateLock lock(LmsApplication::instance());
|
||||||
|
|
||||||
|
Wt::Dbo::Transaction transaction(_db.getSession());
|
||||||
|
|
||||||
|
Database::User::pointer user = _db.getCurrentUser();
|
||||||
|
Database::Track::pointer track = Database::Track::getById(_db.getSession(), trackId);
|
||||||
|
|
||||||
|
if (!track)
|
||||||
|
{
|
||||||
|
LMS_LOG(UI, ERROR) << "Missing track";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user)
|
||||||
|
{
|
||||||
|
LMS_LOG(UI, ERROR) << "Missing user";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Av::TranscodeParameters parameters;
|
||||||
|
|
||||||
|
parameters.setOffset(boost::posix_time::seconds(std::stol(*offsetStr)));
|
||||||
|
parameters.setEncoding(Av::encoding_from_int(std::stol(*encodingStr)));
|
||||||
|
parameters.setBitrate(Av::Stream::Type::Audio, user->getAudioBitrate() );
|
||||||
|
for (std::string strStream: streams)
|
||||||
|
{
|
||||||
|
LMS_LOG(UI, DEBUG) << "Added stream " << std::stol(strStream);
|
||||||
|
parameters.addStream(std::stol(strStream));
|
||||||
|
}
|
||||||
|
|
||||||
|
LMS_LOG(UI, DEBUG) << "Offset set to " << parameters.getOffset();
|
||||||
|
transcoder = std::make_shared<Av::Transcoder>(track->getPath(), parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string mimeType = Av::encoding_to_mimetype(transcoder->getParameters().getEncoding());
|
||||||
|
|
||||||
|
LMS_LOG(UI, DEBUG) << "Mime type set to '" << mimeType << "'";
|
||||||
|
response.setMimeType(mimeType);
|
||||||
|
|
||||||
|
if (!transcoder->start())
|
||||||
|
{
|
||||||
|
LMS_LOG(UI, ERROR) << "Cannot start transcoder";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LMS_LOG(UI, DEBUG) << "Transcoder started";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!transcoder->isComplete())
|
||||||
|
{
|
||||||
|
std::vector<unsigned char> data;
|
||||||
|
data.reserve(_chunkSize);
|
||||||
|
|
||||||
|
transcoder->process(data, _chunkSize);
|
||||||
|
|
||||||
|
response.out().write(reinterpret_cast<char*>(&data[0]), data.size());
|
||||||
|
LMS_LOG(UI, DEBUG) << "Written " << data.size() << " bytes! complete = " << std::boolalpha << transcoder->isComplete();
|
||||||
|
|
||||||
|
if (!response.out())
|
||||||
|
{
|
||||||
|
LMS_LOG(UI, ERROR) << "Write failed!";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!transcoder->isComplete() && response.out()) {
|
||||||
|
continuation = response.createContinuation();
|
||||||
|
continuation->setData(transcoder);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
LMS_LOG(UI, DEBUG) << "No more data!";
|
||||||
|
}
|
||||||
|
catch (std::invalid_argument& e)
|
||||||
|
{
|
||||||
|
LMS_LOG(UI, ERROR) << "Invalid argument: " << e.what();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace UserInterface
|
||||||
|
|
||||||
|
|
||||||
+18
-23
@@ -17,43 +17,38 @@
|
|||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef UI_AUDIO_MOBILE_MEDIA_PLAYER_HPP
|
#pragma once
|
||||||
#define UI_AUDIO_MOBILE_MEDIA_PLAYER_HPP
|
|
||||||
|
|
||||||
#include <Wt/WContainerWidget>
|
#include <mutex>
|
||||||
#include <Wt/WMediaPlayer>
|
|
||||||
#include <Wt/WImage>
|
#include <Wt/WResource>
|
||||||
|
|
||||||
#include "database/DatabaseHandler.hpp"
|
|
||||||
#include "av/AvTranscoder.hpp"
|
#include "av/AvTranscoder.hpp"
|
||||||
|
|
||||||
namespace UserInterface {
|
#include "database/DatabaseHandler.hpp"
|
||||||
namespace Mobile {
|
|
||||||
|
|
||||||
class AudioMediaPlayer : public Wt::WContainerWidget
|
namespace UserInterface {
|
||||||
|
|
||||||
|
class TranscodeResource : public Wt::WResource
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
TranscodeResource(Database::Handler& db, Wt::WObject *parent);
|
||||||
|
~TranscodeResource();
|
||||||
|
|
||||||
static Wt::WMediaPlayer::Encoding getBestEncoding();
|
std::string getUrl(Database::Track::id_type trackId, Av::Encoding encoding = Av::Encoding::OGA, size_t offset_secs = 0, std::vector<size_t> streamIds = {}) const;
|
||||||
|
|
||||||
AudioMediaPlayer(Wt::WMediaPlayer::Encoding encoding, Wt::WContainerWidget *parent = 0);
|
void handleRequest(const Wt::Http::Request& request,
|
||||||
|
Wt::Http::Response& response);
|
||||||
void play(Database::Track::id_type trackId, Av::TranscodeParameters parameters);
|
|
||||||
|
|
||||||
Wt::WMediaPlayer::Encoding getEncoding() const { return _encoding; }
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
Wt::WMediaPlayer* _player;
|
Database::Handler& _db;
|
||||||
Wt::WMediaPlayer::Encoding _encoding;
|
|
||||||
|
|
||||||
Wt::WImage* _cover;
|
|
||||||
Wt::WText* _track;
|
|
||||||
Wt::WText* _artistRelease;
|
|
||||||
|
|
||||||
|
static const std::size_t _chunkSize = 65536*4;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace UserInterface
|
} // namespace UserInterface
|
||||||
} // namespace Mobile
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
|
||||||
@@ -41,7 +41,6 @@ std::string encodingToString(Database::AudioEncoding encoding)
|
|||||||
case Database::AudioEncoding::MP3: return "MP3";
|
case Database::AudioEncoding::MP3: return "MP3";
|
||||||
case Database::AudioEncoding::OGA: return "OGG";
|
case Database::AudioEncoding::OGA: return "OGG";
|
||||||
case Database::AudioEncoding::WEBMA: return "WebM";
|
case Database::AudioEncoding::WEBMA: return "WebM";
|
||||||
case Database::AudioEncoding::FLA: return "Flash";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "?";
|
return "?";
|
||||||
@@ -65,7 +64,7 @@ class AudioFormModel : public Wt::WFormModel
|
|||||||
initializeModels();
|
initializeModels();
|
||||||
|
|
||||||
addField(BitrateField);
|
addField(BitrateField);
|
||||||
addField(EncodingField, "Session must be reloaded to apply this setting");
|
addField(EncodingField);
|
||||||
|
|
||||||
setValidator(BitrateField, new Wt::WValidator(true)); // mandatory
|
setValidator(BitrateField, new Wt::WValidator(true)); // mandatory
|
||||||
setValidator(EncodingField, new Wt::WValidator(true)); // mandatory
|
setValidator(EncodingField, new Wt::WValidator(true)); // mandatory
|
||||||
|
|||||||
@@ -21,12 +21,11 @@
|
|||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
#include <boost/date_time/posix_time/posix_time.hpp>
|
#include <boost/date_time/posix_time/posix_time.hpp>
|
||||||
#include <boost/foreach.hpp>
|
#include <boost/algorithm/string/split.hpp>
|
||||||
|
#include <boost/algorithm/string.hpp>
|
||||||
|
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
|
||||||
namespace MetaData
|
|
||||||
{
|
|
||||||
|
|
||||||
bool readAsPosixTime(const std::string& str, boost::posix_time::ptime& time)
|
bool readAsPosixTime(const std::string& str, boost::posix_time::ptime& time)
|
||||||
{
|
{
|
||||||
@@ -56,11 +55,11 @@ bool readList(const std::string& str, const std::string& separators, std::list<s
|
|||||||
{
|
{
|
||||||
std::string curStr;
|
std::string curStr;
|
||||||
|
|
||||||
BOOST_FOREACH(char c, str) {
|
for (char c : str)
|
||||||
|
{
|
||||||
if (separators.find(c) != std::string::npos) {
|
if (separators.find(c) != std::string::npos) {
|
||||||
if (!curStr.empty()) {
|
if (!curStr.empty()) {
|
||||||
results.push_back(string_to_utf8(curStr));
|
results.push_back(stringToUTF8(curStr));
|
||||||
curStr.clear();
|
curStr.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,11 +72,50 @@ bool readList(const std::string& str, const std::string& separators, std::list<s
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!curStr.empty())
|
if (!curStr.empty())
|
||||||
results.push_back(string_to_utf8(curStr));
|
results.push_back(stringToUTF8(curStr));
|
||||||
|
|
||||||
return !str.empty();
|
return !str.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string
|
||||||
|
durationToString(boost::posix_time::time_duration duration, std::string format)
|
||||||
|
{
|
||||||
|
boost::posix_time::time_facet* facet = new boost::posix_time::time_facet();
|
||||||
|
facet->time_duration_format(format.c_str());
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss.imbue(std::locale(oss.getloc(), facet));
|
||||||
|
oss << duration;
|
||||||
|
|
||||||
|
return oss.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<std::string>
|
||||||
|
splitString(std::string string, std::string separators)
|
||||||
|
{
|
||||||
|
std::vector<std::string> res;
|
||||||
|
|
||||||
|
boost::algorithm::split(res, string, boost::is_any_of(separators), boost::token_compress_on);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string
|
||||||
|
stringTrim(const std::string& str, const std::string& whitespace)
|
||||||
|
{
|
||||||
|
const auto strBegin = str.find_first_not_of(whitespace);
|
||||||
|
if (strBegin == std::string::npos)
|
||||||
|
return ""; // no content
|
||||||
|
|
||||||
|
const auto strEnd = str.find_last_not_of(whitespace);
|
||||||
|
const auto strRange = strEnd - strBegin + 1;
|
||||||
|
|
||||||
|
return str.substr(strBegin, strRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string
|
||||||
|
stringToUTF8(const std::string& str)
|
||||||
|
{
|
||||||
|
return boost::locale::conv::to_utf<char>(str, "UTF-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2013 Emeric Poupon
|
* Copyright (C) 2015 Emeric Poupon
|
||||||
*
|
*
|
||||||
* This file is part of LMS.
|
* This file is part of LMS.
|
||||||
*
|
*
|
||||||
@@ -17,20 +17,32 @@
|
|||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef METADATA_UTILS_HPP
|
#pragma once
|
||||||
#define METADATA_UTILS_HPP
|
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
#include <list>
|
#include <list>
|
||||||
|
|
||||||
#include <boost/locale.hpp>
|
#include <boost/locale.hpp>
|
||||||
|
#include <boost/date_time/posix_time/posix_time_types.hpp>
|
||||||
|
|
||||||
namespace MetaData
|
bool
|
||||||
{
|
readAsPosixTime(const std::string& str, boost::posix_time::ptime& time);
|
||||||
|
|
||||||
bool readAsPosixTime(const std::string& str, boost::posix_time::ptime& time);
|
bool
|
||||||
|
readList(const std::string& str, const std::string& separators, std::list<std::string>& results);
|
||||||
|
|
||||||
bool readList(const std::string& str, const std::string& separators, std::list<std::string>& results);
|
std::string
|
||||||
|
durationToString(boost::posix_time::time_duration duration, std::string format);
|
||||||
|
|
||||||
|
std::vector<std::string>
|
||||||
|
splitString(std::string string, std::string separators);
|
||||||
|
|
||||||
|
std::string
|
||||||
|
stringTrim(const std::string& str, const std::string& whitespace = " \t");
|
||||||
|
|
||||||
|
std::string
|
||||||
|
stringToUTF8(const std::string& str);
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
static inline bool readAs(const std::string& str, T& data)
|
static inline bool readAs(const std::string& str, T& data)
|
||||||
@@ -40,30 +52,3 @@ static inline bool readAs(const std::string& str, T& data)
|
|||||||
return !iss.fail();
|
return !iss.fail();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
std::string
|
|
||||||
static inline string_trim(const std::string& str,
|
|
||||||
const std::string& whitespace = " \t")
|
|
||||||
{
|
|
||||||
const auto strBegin = str.find_first_not_of(whitespace);
|
|
||||||
if (strBegin == std::string::npos)
|
|
||||||
return ""; // no content
|
|
||||||
|
|
||||||
const auto strEnd = str.find_last_not_of(whitespace);
|
|
||||||
const auto strRange = strEnd - strBegin + 1;
|
|
||||||
|
|
||||||
return str.substr(strBegin, strRange);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
std::string
|
|
||||||
static inline string_to_utf8(const std::string& str)
|
|
||||||
{
|
|
||||||
return boost::locale::conv::to_utf<char>(str, "UTF-8");
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace MetaData
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
@@ -185,8 +185,6 @@ int main(void)
|
|||||||
assert(res.front().id() == 1);
|
assert(res.front().id() == 1);
|
||||||
assert(res.front()->getName() == "release01");
|
assert(res.front()->getName() == "release01");
|
||||||
|
|
||||||
// Make sure release has an artist
|
|
||||||
assert(res.front()->getArtists().size() == 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select genre by name
|
// Select genre by name
|
||||||
|
|||||||
+44
-2
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
TESTS = database-basics database-integrity sql-query database-user
|
TESTS = database-basics database-integrity sql-query database-user
|
||||||
|
|
||||||
check_PROGRAMS = database-basics database-integrity sql-query database-user test-wt
|
check_PROGRAMS = database-basics database-integrity sql-query database-user test-wt test-avmetadata test-avtranscoder test-wt-audio
|
||||||
|
|
||||||
database_basics_SOURCES = \
|
database_basics_SOURCES = \
|
||||||
$(srcdir)/CheckDbBasics.cpp \
|
$(srcdir)/CheckDbBasics.cpp \
|
||||||
@@ -60,6 +60,48 @@ sql_query_CXXFLAGS=-std=c++11 -Wall -Wextra -I$(top_srcdir)/src
|
|||||||
|
|
||||||
|
|
||||||
test_wt_SOURCES = TestWt.cpp
|
test_wt_SOURCES = TestWt.cpp
|
||||||
|
|
||||||
test_wt_CXXFLAGS=-std=c++11 -Wall -Wextra
|
test_wt_CXXFLAGS=-std=c++11 -Wall -Wextra
|
||||||
|
|
||||||
|
test_wt_audio_SOURCES = TestWtAudio.cpp\
|
||||||
|
$(top_srcdir)/src/logger/Logger.cpp \
|
||||||
|
$(top_srcdir)/src/utils/Utils.cpp \
|
||||||
|
$(top_srcdir)/src/metadata/AvFormat.cpp \
|
||||||
|
$(top_srcdir)/src/av/AvInfo.cpp \
|
||||||
|
$(top_srcdir)/src/av/AvTranscoder.cpp \
|
||||||
|
$(top_srcdir)/src/cover/CoverArtGrabber.cpp \
|
||||||
|
$(top_srcdir)/src/database/Artist.cpp \
|
||||||
|
$(top_srcdir)/src/database/Playlist.cpp \
|
||||||
|
$(top_srcdir)/src/database/Track.cpp \
|
||||||
|
$(top_srcdir)/src/database/DatabaseHandler.cpp \
|
||||||
|
$(top_srcdir)/src/database/MediaDirectory.cpp \
|
||||||
|
$(top_srcdir)/src/database/Release.cpp \
|
||||||
|
$(top_srcdir)/src/database/SearchFilter.cpp \
|
||||||
|
$(top_srcdir)/src/database/SqlQuery.cpp \
|
||||||
|
$(top_srcdir)/src/database/User.cpp \
|
||||||
|
$(top_srcdir)/src/database/Video.cpp \
|
||||||
|
$(top_srcdir)/src/image/Image.cpp \
|
||||||
|
$(top_srcdir)/src/ui/resource/CoverResource.cpp \
|
||||||
|
$(top_srcdir)/src/ui/resource/TranscodeResource.cpp
|
||||||
|
|
||||||
|
test_wt_audio_CXXFLAGS=-std=c++11 -Wall -Wextra -I$(top_srcdir)/src $(MAGICKXX_CFLAGS)
|
||||||
|
test_wt_audio_LDADD=$(MAGICKXX_LIBS)
|
||||||
|
|
||||||
|
|
||||||
|
test_avmetadata_SOURCES = TestAvMetadata.cpp \
|
||||||
|
$(top_srcdir)/src/logger/Logger.cpp \
|
||||||
|
$(top_srcdir)/src/utils/Utils.cpp \
|
||||||
|
$(top_srcdir)/src/metadata/AvFormat.cpp \
|
||||||
|
$(top_srcdir)/src/av/AvInfo.cpp
|
||||||
|
|
||||||
|
test_avmetadata_CXXFLAGS=-std=c++11 -Wall -Wextra -I$(top_srcdir)/src
|
||||||
|
|
||||||
|
|
||||||
|
test_avtranscoder_SOURCES = TestAvTranscoder.cpp \
|
||||||
|
$(top_srcdir)/src/logger/Logger.cpp \
|
||||||
|
$(top_srcdir)/src/utils/Utils.cpp \
|
||||||
|
$(top_srcdir)/src/av/AvInfo.cpp \
|
||||||
|
$(top_srcdir)/src/av/AvTranscoder.cpp
|
||||||
|
|
||||||
|
test_avtranscoder_CXXFLAGS=-std=c++11 -Wall -Wextra -I$(top_srcdir)/src
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include "av/AvInfo.hpp"
|
||||||
|
#include "metadata/AvFormat.hpp"
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
if (argc != 2)
|
||||||
|
{
|
||||||
|
std::cerr << "Usage: <file>" << std::endl;
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Av::AvInit();
|
||||||
|
|
||||||
|
MetaData::AvFormat parser;
|
||||||
|
MetaData::Items items;
|
||||||
|
|
||||||
|
if (!parser.parse(argv[1], items))
|
||||||
|
{
|
||||||
|
std::cout << "Parsing failed" << std::endl;
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto item : items)
|
||||||
|
{
|
||||||
|
switch (item.first)
|
||||||
|
{
|
||||||
|
case MetaData::Type::TrackNumber:
|
||||||
|
std::cout << "Track: " << boost::any_cast<std::size_t>(item.second) << std::endl;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MetaData::Type::TotalTrack:
|
||||||
|
std::cout << "TotalTrack: " << boost::any_cast<std::size_t>(item.second) << std::endl;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MetaData::Type::DiscNumber:
|
||||||
|
std::cout << "Disc: " << boost::any_cast<std::size_t>(item.second) << std::endl;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MetaData::Type::TotalDisc:
|
||||||
|
std::cout << "TotalDisc: " << boost::any_cast<std::size_t>(item.second) << std::endl;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
}
|
||||||
|
catch (std::exception& e)
|
||||||
|
{
|
||||||
|
std::cerr << "Caught exception: " << e.what();
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include "av/AvInfo.hpp"
|
||||||
|
#include "av/AvTranscoder.hpp"
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
if (argc != 2)
|
||||||
|
{
|
||||||
|
std::cerr << "Usage: <file>" << std::endl;
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Av::AvInit();
|
||||||
|
Av::Transcoder::init();
|
||||||
|
|
||||||
|
// Make pstream work with ffmpeg
|
||||||
|
close(STDIN_FILENO);
|
||||||
|
|
||||||
|
Av::TranscodeParameters parameters;
|
||||||
|
parameters.setEncoding(Av::Encoding::MP3);
|
||||||
|
parameters.setOffset( boost::posix_time::seconds(0) );
|
||||||
|
parameters.setBitrate( Av::Stream::Type::Audio, 160000 );
|
||||||
|
// parameters.addStream(0);
|
||||||
|
|
||||||
|
Av::Transcoder transcoder(argv[1], parameters);
|
||||||
|
|
||||||
|
if (!transcoder.start())
|
||||||
|
throw std::runtime_error("transcoder.start failed!");
|
||||||
|
|
||||||
|
while (!transcoder.isComplete())
|
||||||
|
{
|
||||||
|
std::vector<unsigned char> data;
|
||||||
|
std::cout << "Processing ..." << std::endl;
|
||||||
|
transcoder.process(data, 65536);
|
||||||
|
std::cout << "Processing done" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "Complete!" << std::endl;
|
||||||
|
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
}
|
||||||
|
catch (std::exception& e)
|
||||||
|
{
|
||||||
|
std::cerr << "Caught exception: " << e.what();
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,322 @@
|
|||||||
|
|
||||||
|
#include <Wt/WServer>
|
||||||
|
#include <Wt/WApplication>
|
||||||
|
#include <Wt/WContainerWidget>
|
||||||
|
#include <Wt/WText>
|
||||||
|
#include <Wt/WAudio>
|
||||||
|
#include <Wt/WPushButton>
|
||||||
|
#include <Wt/WBootstrapTheme>
|
||||||
|
#include <Wt/WTemplate>
|
||||||
|
#include <Wt/WLineEdit>
|
||||||
|
#include <Wt/WImage>
|
||||||
|
|
||||||
|
#include "database/DatabaseHandler.hpp"
|
||||||
|
|
||||||
|
#include "ui/resource/TranscodeResource.hpp"
|
||||||
|
#include "ui/resource/CoverResource.hpp"
|
||||||
|
|
||||||
|
|
||||||
|
class InputRange : public Wt::WWebWidget
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
InputRange(Wt::WContainerWidget *parent = 0)
|
||||||
|
: Wt::WWebWidget(parent)
|
||||||
|
{
|
||||||
|
setHtmlTagName("input");
|
||||||
|
setAttributeValue("type", "range");
|
||||||
|
}
|
||||||
|
|
||||||
|
Wt::DomElementType domElementType() const
|
||||||
|
{
|
||||||
|
return Wt::DomElement_INPUT;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Wt::WString MyPlayerTemplate = "${shuffle} ${repeat} ${playlist} ${prev} ${play-pause} ${next} ${cover} ${artist} ${track} ${release} ${curtime} ${seekbar} ${duration} ${volume}";
|
||||||
|
|
||||||
|
class MyPlayer : public Wt::WContainerWidget
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
|
||||||
|
void playbackComplete(void)
|
||||||
|
{
|
||||||
|
// Switch to the next track
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadTrack(Database::Track::id_type trackId)
|
||||||
|
{
|
||||||
|
Wt::Dbo::Transaction transaction(_db.getSession());
|
||||||
|
|
||||||
|
Database::Track::pointer track = Database::Track::getById(_db.getSession(), trackId);
|
||||||
|
if (!track)
|
||||||
|
{
|
||||||
|
std::cerr << "no track for this id!" << std::endl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_trackName->setText(Wt::WString::fromUTF8(track->getName()));
|
||||||
|
_artistName->setText( Wt::WString::fromUTF8(track->getArtist()->getName()));
|
||||||
|
_releaseName->setText( Wt::WString::fromUTF8(track->getRelease()->getName()));
|
||||||
|
_cover->setImageLink(_coverResource->getTrackUrl(trackId, 64));
|
||||||
|
_trackDuration->setText( boost::posix_time::to_simple_string( track->getDuration() ));
|
||||||
|
|
||||||
|
// Analyse track, select the best media stream
|
||||||
|
Av::MediaFile mediaFile(track->getPath());
|
||||||
|
|
||||||
|
if (!mediaFile.open() || !mediaFile.scan())
|
||||||
|
{
|
||||||
|
std::cerr << "cannot open file '" << track->getPath() << std::endl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int audioBestStreamId = mediaFile.getBestStreamId(Av::Stream::Type::Audio);
|
||||||
|
std::vector<std::size_t> streams;
|
||||||
|
if (audioBestStreamId != -1)
|
||||||
|
streams.push_back(audioBestStreamId);
|
||||||
|
|
||||||
|
this->doJavaScript("\
|
||||||
|
document.lms.audio.state = \"loaded\";\
|
||||||
|
document.lms.audio.seekbar.min = " + std::to_string(0) + ";\
|
||||||
|
document.lms.audio.seekbar.max = " + std::to_string(track->getDuration().total_seconds()) + ";\
|
||||||
|
document.lms.audio.seekbar.value = 0;\
|
||||||
|
document.lms.audio.seekbar.disabled = false;\
|
||||||
|
document.lms.audio.offset = 0;\
|
||||||
|
document.lms.audio.curTime = 0;\
|
||||||
|
");
|
||||||
|
|
||||||
|
_audio->pause();
|
||||||
|
_audio->clearSources();
|
||||||
|
_audio->addSource(_transcodeResource->getUrl(trackId, Av::Encoding::MP3, 0, streams));
|
||||||
|
_audio->play();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
MyPlayer(Database::Handler &db, Wt::WContainerWidget *parent = 0)
|
||||||
|
: Wt::WContainerWidget(parent),
|
||||||
|
_db(db)
|
||||||
|
{
|
||||||
|
|
||||||
|
_transcodeResource = new UserInterface::TranscodeResource(db, this);
|
||||||
|
_coverResource = new UserInterface::CoverResource(db, this);
|
||||||
|
|
||||||
|
Wt::WTemplate *t = new Wt::WTemplate(MyPlayerTemplate, this);
|
||||||
|
|
||||||
|
_audio = new Wt::WAudio(this);
|
||||||
|
|
||||||
|
_cover = new Wt::WImage();
|
||||||
|
t->bindWidget("cover", _cover);
|
||||||
|
_cover->setImageLink(_coverResource->getUnknownTrackUrl(64));
|
||||||
|
|
||||||
|
InputRange *seekbar = new InputRange();
|
||||||
|
t->bindWidget("seekbar", seekbar);
|
||||||
|
|
||||||
|
_trackName = new Wt::WText();
|
||||||
|
t->bindWidget("track", _trackName);
|
||||||
|
|
||||||
|
_artistName = new Wt::WText();
|
||||||
|
t->bindWidget("artist", _artistName);
|
||||||
|
|
||||||
|
_releaseName = new Wt::WText();
|
||||||
|
t->bindWidget("release", _releaseName);
|
||||||
|
|
||||||
|
InputRange *volumeSlider = new InputRange();
|
||||||
|
t->bindWidget("volume", volumeSlider);
|
||||||
|
|
||||||
|
Wt::WPushButton *playlistBtn = new Wt::WPushButton("<i class=\"fa fa-list fa-lg\"></i>", Wt::XHTMLText);
|
||||||
|
t->bindWidget("playlist", playlistBtn);
|
||||||
|
|
||||||
|
Wt::WPushButton *repeatBtn = new Wt::WPushButton("<i class=\"fa fa-repeat fa-lg\"></i>", Wt::XHTMLText);
|
||||||
|
t->bindWidget("repeat", repeatBtn);
|
||||||
|
|
||||||
|
Wt::WPushButton *shuffleBtn = new Wt::WPushButton("<i class=\"fa fa-random fa-lg\"></i>", Wt::XHTMLText);
|
||||||
|
t->bindWidget("shuffle", shuffleBtn);
|
||||||
|
|
||||||
|
Wt::WPushButton *prevBtn = new Wt::WPushButton("<i class=\"fa fa-step-backward fa-lg\"></i>", Wt::XHTMLText);
|
||||||
|
t->bindWidget("prev", prevBtn);
|
||||||
|
|
||||||
|
Wt::WPushButton *nextBtn = new Wt::WPushButton("<i class=\"fa fa-step-forward fa-lg\"></i>", Wt::XHTMLText);
|
||||||
|
t->bindWidget("next", nextBtn);
|
||||||
|
|
||||||
|
Wt::WPushButton *playPauseBtn = new Wt::WPushButton("<i class=\"fa fa-play fa-lg\"></i>", Wt::XHTMLText);
|
||||||
|
t->bindWidget("play-pause", playPauseBtn);
|
||||||
|
|
||||||
|
Wt::WText *trackCurrentTime = new Wt::WText("00:00");
|
||||||
|
t->bindWidget("curtime", trackCurrentTime);
|
||||||
|
|
||||||
|
_trackDuration = new Wt::WText("00:00");
|
||||||
|
t->bindWidget("duration", _trackDuration);
|
||||||
|
|
||||||
|
//Wt::WTemplate *_volumeSlider = new Wt::WTemplate(VolumeSliderTemplate, this);
|
||||||
|
|
||||||
|
this->doJavaScript(
|
||||||
|
"\
|
||||||
|
document.lms = {};\
|
||||||
|
document.lms.audio = {};\
|
||||||
|
document.lms.audio.audio = " + _audio->jsRef() + ";\
|
||||||
|
document.lms.audio.seekbar = " + seekbar->jsRef() +";\
|
||||||
|
document.lms.audio.volumeSlider = " + volumeSlider->jsRef() + ";\
|
||||||
|
document.lms.audio.curTimeText = " + trackCurrentTime->jsRef() + ";\
|
||||||
|
document.lms.audio.playPause = " + playPauseBtn->jsRef() + ";\
|
||||||
|
\
|
||||||
|
document.lms.audio.offset = 0;\
|
||||||
|
document.lms.audio.curTime = 0;\
|
||||||
|
document.lms.audio.state = \"init\";\
|
||||||
|
document.lms.audio.volume = 1;\
|
||||||
|
\
|
||||||
|
document.lms.audio.seekbar.value = 0;\
|
||||||
|
document.lms.audio.seekbar.disabled = true;\
|
||||||
|
\
|
||||||
|
document.lms.audio.volumeSlider.min = 0;\
|
||||||
|
document.lms.audio.volumeSlider.max = 100;\
|
||||||
|
document.lms.audio.volumeSlider.value = 100;\
|
||||||
|
\
|
||||||
|
function updateUI() {\
|
||||||
|
document.lms.audio.curTimeText.innerHTML = document.lms.audio.curTime;\
|
||||||
|
document.lms.audio.seekbar.value = document.lms.audio.curTime;\
|
||||||
|
}\
|
||||||
|
\
|
||||||
|
var mouseDown = 0;\
|
||||||
|
function seekMouseDown(e) {\
|
||||||
|
++mouseDown;\
|
||||||
|
}\
|
||||||
|
function seekMouseUp(e) {\
|
||||||
|
--mouseDown;\
|
||||||
|
}\
|
||||||
|
\
|
||||||
|
function seeking(e) {\
|
||||||
|
if (document.lms.audio.state == \"init\")\
|
||||||
|
return;\
|
||||||
|
\
|
||||||
|
document.lms.audio.curTimeText.innerHTML = document.lms.audio.seekbar.value;\
|
||||||
|
}\
|
||||||
|
\
|
||||||
|
function seek(e) {\
|
||||||
|
if (document.lms.audio.state == \"init\")\
|
||||||
|
return;\
|
||||||
|
\
|
||||||
|
document.lms.audio.audio.pause(); \
|
||||||
|
document.lms.audio.offset = parseInt(document.lms.audio.seekbar.value);\
|
||||||
|
document.lms.audio.curTime = document.lms.audio.seekbar.value;\
|
||||||
|
var audioSource = document.lms.audio.audio.getElementsByTagName(\"source\")[0];\
|
||||||
|
var src = audioSource.src;\
|
||||||
|
src = src.slice(0, src.lastIndexOf(\"=\") + 1);\
|
||||||
|
audioSource.src = src + document.lms.audio.seekbar.value;\
|
||||||
|
document.lms.audio.audio.load(); \
|
||||||
|
document.lms.audio.audio.play(); \
|
||||||
|
document.lms.audio.curTimeText.innerHTML = ~~document.lms.audio.curTime + \" \";\
|
||||||
|
}\
|
||||||
|
\
|
||||||
|
function volumeChanged() {\
|
||||||
|
document.lms.audio.audio.volume = document.lms.audio.volumeSlider.value / 100;\
|
||||||
|
}\
|
||||||
|
\
|
||||||
|
function updateCurTime() {\
|
||||||
|
document.lms.audio.curTime = document.lms.audio.offset + ~~document.lms.audio.audio.currentTime; \
|
||||||
|
if (mouseDown == 0)\
|
||||||
|
updateUI();\
|
||||||
|
} \
|
||||||
|
\
|
||||||
|
function playPause() {\
|
||||||
|
if (document.lms.audio.state == \"init\") \
|
||||||
|
return;\
|
||||||
|
\
|
||||||
|
if (document.lms.audio.audio.paused)\
|
||||||
|
document.lms.audio.audio.play();\
|
||||||
|
else\
|
||||||
|
document.lms.audio.audio.pause();\
|
||||||
|
\
|
||||||
|
}\
|
||||||
|
\
|
||||||
|
document.lms.audio.audio.addEventListener('timeupdate', updateCurTime); \
|
||||||
|
document.lms.audio.seekbar.addEventListener('change', seek);\
|
||||||
|
document.lms.audio.seekbar.addEventListener('input', seeking);\
|
||||||
|
document.lms.audio.seekbar.addEventListener('mousedown', seekMouseDown);\
|
||||||
|
document.lms.audio.seekbar.addEventListener('mouseup', seekMouseUp);\
|
||||||
|
document.lms.audio.volumeSlider.addEventListener('input', volumeChanged);\
|
||||||
|
document.lms.audio.playPause.addEventListener('click', playPause);\
|
||||||
|
"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
UserInterface::TranscodeResource* _transcodeResource;
|
||||||
|
UserInterface::CoverResource* _coverResource;
|
||||||
|
Database::Handler& _db;
|
||||||
|
Wt::WAudio* _audio;
|
||||||
|
Wt::WText* _trackDuration;
|
||||||
|
Wt::WText* _trackName;
|
||||||
|
Wt::WText* _artistName;
|
||||||
|
Wt::WText* _releaseName;
|
||||||
|
Wt::WImage* _cover;
|
||||||
|
};
|
||||||
|
|
||||||
|
class TestApplication : public Wt::WApplication
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
TestApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnectionPool& connectionPool)
|
||||||
|
: Wt::WApplication(env)
|
||||||
|
, _db(connectionPool)
|
||||||
|
{
|
||||||
|
Wt::WBootstrapTheme *bootstrapTheme = new Wt::WBootstrapTheme(this);
|
||||||
|
bootstrapTheme->setVersion(Wt::WBootstrapTheme::Version3);
|
||||||
|
bootstrapTheme->setResponsive(true);
|
||||||
|
setTheme(bootstrapTheme);
|
||||||
|
|
||||||
|
useStyleSheet("resources/font-awesome/css/font-awesome.min.css");
|
||||||
|
|
||||||
|
Wt::WLineEdit* trackSelector = new Wt::WLineEdit();
|
||||||
|
MyPlayer* player = new MyPlayer(_db);
|
||||||
|
|
||||||
|
trackSelector->changed().connect(std::bind([=] {
|
||||||
|
player->loadTrack(Wt::asNumber(trackSelector->valueText()));
|
||||||
|
}));
|
||||||
|
|
||||||
|
root()->addWidget(trackSelector);
|
||||||
|
root()->addWidget(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Database::Handler _db;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
static Wt::WApplication *createTestApplication(const Wt::WEnvironment& env, Wt::Dbo::SqlConnectionPool& connectionPool )
|
||||||
|
{
|
||||||
|
return new TestApplication(env, connectionPool);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Av::AvInit();
|
||||||
|
Av::Transcoder::init();
|
||||||
|
|
||||||
|
Wt::WServer server(argv[0]);
|
||||||
|
server.setServerConfiguration (argc, argv);
|
||||||
|
|
||||||
|
// Make pstream work with ffmpeg
|
||||||
|
close(STDIN_FILENO);
|
||||||
|
|
||||||
|
Database::Handler::configureAuth();
|
||||||
|
std::unique_ptr<Wt::Dbo::SqlConnectionPool> connectionPool( Database::Handler::createConnectionPool("/var/lms/lms.db"));
|
||||||
|
|
||||||
|
server.addEntryPoint(Wt::Application, boost::bind(createTestApplication, _1, boost::ref(*connectionPool)));
|
||||||
|
|
||||||
|
server.start();
|
||||||
|
|
||||||
|
Wt::WServer::waitForShutdown(argv[0]);
|
||||||
|
|
||||||
|
server.stop();
|
||||||
|
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
}
|
||||||
|
catch (std::exception& e)
|
||||||
|
{
|
||||||
|
std::cerr << "Caught exception: " << e.what();
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user