diff --git a/Makefile.am b/Makefile.am index 588149ae..99025d4e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -3,3 +3,23 @@ AUTOMAKE_OPTIONS = dist-bzip2 no-dist-gzip SUBDIRS = test src +lms_docrootdir=$(pkgdatadir)/docroot +lms_approotdir=$(pkgdatadir)/approot + +lms_cssdir=$(lms_docrootdir)/css +lms_imagesdir=$(lms_docrootdir)/images + +lms_css_DATA = \ + docroot/css/lms.css + +lms_images_DATA = \ + docroot/images/unknown-cover.jpg + +lms_approot_DATA = \ + approot/templates.xml + +install-data-hook: + ln -s $(datadir)/Wt/resources $(lms_docrootdir)/resources || echo "Skipped symlink creation" + +uninstall-local: + cd $(lms_docrootdir) && rm -f -r $(lms_docrootdir)/resources diff --git a/README b/README index dd778bc4..96dc92fb 100644 --- a/README +++ b/README @@ -1,70 +1 @@ -LMS - Lightweight Media Server - - -LMS is a self-hosted media streaming software, released under the GPLv3 license. -It allows you to access your music and your videos using an https web interface. - -Features: -- Winamp-like interface, suited for large databases -- Audio/Video transcode for maximum interoperability and low bandwith requirements -- User management -- LMS API available in Protocol Buffers (see src/lms-api/proto/messages.proto) - -Please note LMS is still under development and will gain more features in the future. -A mobile application based on Kivy is being developped as well. - -LMS is written entirely in C++. Therefore, it is suitable to run on embedded devices, where space and/or memory are limited. -Please note some media files may require significant CPU usage to be transcoded. - -I) DEPENDENCIES - -Debian packages: -g++ autoconf automake libboost-dev libavcodec-dev libwtdbosqlite-dev libwthttp-dev libwtdbo-dev libwt-dev libprotobuf-dev libconfig++-dev libprotobuf-dev protobuf-compiler libjpeg8-dev libavcodec-dev libavformat-dev libavutil-dev - -Warning: your distrib may provide an outdated wt library. Using the latest Wt (>= 3.3.3) is highly recommended (see http://www.webtoolkit.eu/wt) - - -II) BUILD - -# autoreconf -vfi -# mkdir build -# cd build -# ../configure - -(configure will complain if a mandatory library is missing) - -The configure script allows you to disable the LMS API support (and protobuf dependencies) -See 'configure --help' for more information. - -# make - -III) Setting up SSL materials (required by the SSL server and the web server) - -This is just a self signed certificate example, you could do use a CA if you want. - -Generate a dh file: -# openssl dhparam -out dh2048.pem 2048 - -Generate a self signed certificate: -# openssl req -x509 -out cert.pem -keyout privkey.pem -newkey rsa:4096 - - -IV) RUNNING - -In the build directory: -# ./src/lms lms.conf - -Create the lms.conf file using the etc/lms.sample.conf -- "ui.resources.approot" has to refer to the ui/approot/ directory. -- "ui.resources.docroot" has to refer to the ui/docroot/ directory -You must also link Wt's docroot (usually located in /usr/share/Wt) into that directory. Example: -# ln -s /var/lms/docroot/resources /usr/share/Wt/resources - -Depending on your SSL parameters, you may be asked for the PEM passphrase to unlock the private key. - -V) LINKS - -- Wt (http://www.webtoolkit.eu/) -- bootstrap3 (http://getbootstrap.com/) -- libav project (https://www.libav.org/). -- Protocol Buffers (https://github.com/google/protobuf/) +See README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..76a47db4 --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# LMS - Lightweight Media Server + +LMS is a self-hosted media streaming software, released under the GPLv3 license. +It allows you to access your music and your videos using an https web interface. + +## Features + - Winamp-like interface, suited for large databases + - Audio/Video transcode for maximum interoperability and low bandwith requirements + - User management + +Please note LMS is still under development and will gain more features in the future. + +LMS is written entirely in C++. Therefore, it is suitable to run on embedded devices, where space and/or memory are limited. +Please note some media files may require significant CPU usage to be transcoded. + +## Dependencies +### Debian or Ubuntu packages + +```sh +$ apt-get install g++ autoconf automake libboost-dev libboost-locale-dev libboost-iostreams-dev libavcodec-dev libwtdbosqlite-dev libwthttp-dev libwtdbo-dev libwt-dev libmagick++-dev libavcodec-dev libavformat-dev libav-tools libpstreams-dev +``` + +## Build + +```sh +$ autoreconf -vfi +$ mkdir build +$ cd build +$ ../configure --prefix=/usr +``` +configure will complain if a mandatory library is missing. + +```sh +$ make -j 4 +``` + +## Install + +```sh +$ make install +``` +This command requires root privileges + +## Running + +```sh +$ /usr/bin/lms --docroot='/usr/share/lms/docroot/;/resources,/css,/images' --approot=/usr/share/lms/approot --http-address 0.0.0.0 --http-port 5081 +``` +It is highly recommended to run LMS as a non root user. +The exectuable needs write accesses to the /var/lms/ directory. + +## Setting up SSL materials (optional) +Here is just a self signed certificate example, you could do use a CA if you want. + +Generate a dh file: +```sh +$ openssl dhparam -out dh2048.pem 2048 +``` +Generate a self signed certificate: +```sh +$ openssl req -x509 -out cert.pem -keyout privkey.pem -newkey rsa:4096 +``` +Run: +```sh +$ /usr/bin/lms --docroot='/usr/share/lms/docroot/;/resources,/css,/images' --approot=/usr/share/lms/approot --https-address 0.0.0.0 --https-port 5081 --ssl-certificate cert.pem --ssl-private-key privkey.pem --ssl-tmp-dh dh2048.pem +``` +Depending on your SSL parameters, you may be asked for the PEM passphrase to unlock the private key. + +## Credits + +- Wt (http://www.webtoolkit.eu/) +- bootstrap3 (http://getbootstrap.com/) +- libav project (https://www.libav.org/). diff --git a/TODO b/TODO index 51a0ca71..ab2db6e2 100644 --- a/TODO +++ b/TODO @@ -3,16 +3,17 @@ - Make the feature [Cover] -- Scaling: find something more "reliable" than GIL and its custom extensions (adobe work, io_new)? -- Handle several file formats (not only jpg) - Handle preferred cover file names - Implement a cache and a grabber from some web service (mandatory for artists) [Database] - Optim, use SQL query to get the "genre" orphans - Use Inotify like system to watch modified/added files? -- count scan import errors - handle access rights problems (instead of aborting) +- add a global play counter for tracks. This will help people to spot most popular files +- rework the exception process in av/metadata/updater in case of bad files +- Use the WServer::post method to notify the end of the database scan? (with results?) +- Put more config information in the database, access from the UI [Metadata] - OGG metadata -> properly handle metadata nested in the audio stream @@ -22,23 +23,18 @@ [Users] - Handle login lifetime? -- Limit multi login from UI and remote interfaces (limit per interface is acceptable) - -[ServiceManager] -- Rework the whole start/stop/try/cach/thread/interrupts things -- Use our own WIOService +- Use the WServer::post method to kick users already logged in? +- Limit multi login from UI and API interfaces (limit per interface is acceptable) [Services] - consider adding support for external web servers (new FCGI service?) -[Transcode] -- some early end of playback spotted on files. Maybe this is because the stderr of the forked process is not properly closed? - [UI] - handle internationalization [Settings] - logout users that are being changed (loss of admin admin rights), or make sure they are still admin when they make changes - "signal not exposed" problem if a user logout and login again. Bad resource destruction? + - add a scroll area in order for settings to work on mobile devices [user/transcoding] - Prefered codecs for video - use a slider instead of a combo box for the bitrates? @@ -52,9 +48,7 @@ [Audio] - Style eveything nicely... - MediaPlayer: move the slider using js (http://redmine.webtoolkit.eu/boards/2/topics/7924?r=8478, http://redmine.emweb.be/boards/2/topics/10994) - - TrackView: handle durations > 1 hour - TrackView: Reselect the current selected item when displaying the updated search results - - TrackView: display the total duration of the track query - Add covers in the release filter? - Add a download button to get the current playlist in a streamed zip file - Add a upload button to upload media files in a dedicated directory @@ -69,11 +63,8 @@ - Make a dedicated audio layout in mobile environment (keep the same layout for videos?) - Handle internal path -[Lms API] -- Implement partial text search options in the GetXXX messages - [REST API] -- Make another dedicated REST API. Maybe use the SubSonic API or Ampache API? +- Make a dedicated REST API. Maybe use the SubSonic API or Ampache API? [Logs] -- Capture transcoder log output to get more information on errors +- Capture transcoder log output to get more information on errors? diff --git a/src/ui/approot/templates.xml b/approot/templates.xml similarity index 94% rename from src/ui/approot/templates.xml rename to approot/templates.xml index b55f513b..1e9d1148 100644 --- a/src/ui/approot/templates.xml +++ b/approot/templates.xml @@ -311,6 +311,30 @@ +
+ +
+ ${audio-file-extensions} +
+
+ ${audio-file-extensions-info} +
+
+ +
+ +
+ ${video-file-extensions} +
+
+ ${video-file-extensions-info} +
+
+
${apply-button} ${discard-button} ${immediate-scan-button} diff --git a/conf/lms.conf.sample b/conf/lms.conf.sample deleted file mode 100644 index 01ffec79..00000000 --- a/conf/lms.conf.sample +++ /dev/null @@ -1,63 +0,0 @@ - -main = { - - logger = { - level = 7; # level common for all loggers - file = { - enable = true; - path = "/var/lms/lms.log"; # comment to disable file logging - } - console = { - enable = true; - } - } - - database = { - path = "/var/lms/lms.db"; - - audio_extensions = "mp3 ogg oga aac m4a flac wav wma aif aiff ape mpc shn"; - video_extensions = "flv avi mpg mpeg mp4 m4v mkv mov wmv ogv divx m2ts"; - } - - cover = { - file_extensions = "jpg jpeg"; - file_max_size = 5000000; - file_preferred_names = "cover front"; - } -} - - -ui = { - resources = { - docroot = "/var/lms/docroot" - approot = "/var/lms/approot" - } - - listen-endpoint = { - port = 5081; - addr = "0.0.0.0"; - } - - ssl-crypto = { - cert = "/var/lms/certs/certUI.pem"; - key = "/var/lms/private/privkeyUI.pem"; - dh = "/var/lms/dh/dh2048.pem"; - } - -} - -remote = { - nb-threads = 1; - - listen-endpoint = { - port = 5080; - addr = "0.0.0.0"; - } - - ssl-crypto = { - cert = "/var/lms/certs/certRemote.pem"; - key = "/var/lms/private/privkeyRemote.pem"; - dh = "/var/lms/dh/dh2048.pem"; - } -} - diff --git a/configure.ac b/configure.ac index e7408b53..39773056 100644 --- a/configure.ac +++ b/configure.ac @@ -8,55 +8,46 @@ AC_LANG_CPLUSPLUS # Checks for programs AC_PROG_CXX +AC_ARG_ENABLE([video], AS_HELP_STRING([--enable-video], + [Enable Video support. Default to no.])], + [enable_video="$enableval"], + [enable_video="no"]) -AC_ARG_ENABLE(lmsapi, AS_HELP_STRING([--enable-lmsapi], - [Build with the lmsapi protocol support. Defaults to yes.])], - [enable_lmsapi="$enableval"], - [enable_lmsapi="yes"]) +AM_CONDITIONAL([VIDEO], [test x$enable_video = xyes]) +AS_IF([test x$enable_video = xyes], [AC_DEFINE(HAVE_VIDEO, [1], [Enable Video support])]) -# ProtoBuf compiler. -# First, specify with --with-protoc=/path/of/protoc. -# Or, specify with env variable PROTOC. -# If neither of the above, find it in the path. -AS_IF([test "$enable_lmsapi" = "yes"], - [AC_ARG_WITH([protoc], - [AS_HELP_STRING([--with-protoc=/path/of/protoc], - [Location of the protocol buffers compiler protoc. Defaults to looking on path.])], - [PROTOC="$withval"], - [ AS_IF([test "x${PROTOC}" == "x"], - [AC_PATH_PROG([PROTOC], [protoc], [no])])] - ) - ] - [ AS_IF([test "${PROTOC}" == "no"], [AC_MSG_ERROR([ProtoBuf compiler "protoc" not found.])]) ] - [AC_CHECK_LIB( [protobuf], - [main], - , - [AC_MSG_ERROR([libprotobuf not found!])])] - [lmsapi=true] - , - [lmsapi=false]) +PKG_CHECK_MODULES(IMAGEMAGICKXX, "ImageMagick++", [ HAVE_IMAGEMAGICKXX=yes ], [ ]) +if test -n "$HAVE_IMAGEMAGICKXX"; then + MAGICKXX_CFLAGS="$IMAGEMAGICKXX_CFLAGS" + MAGICKXX_LIBS="$IMAGEMAGICKXX_LIBS" +fi +AC_SUBST(MAGICKXX_CFLAGS) +AC_SUBST(MAGICKXX_LIBS) -AM_CONDITIONAL([LMSAPI], [test x$lmsapi = xtrue]) -AS_IF([test x$lmsapi = xtrue], [AC_DEFINE(HAVE_LMSAPI, [1], [Enable LMS API support])]) - -AC_CHECK_HEADERS([Wt/WApplication jpeglib.h], +AC_CHECK_HEADERS([Wt/WApplication pstreams/pstream.h], [], [AC_MSG_ERROR([Header not found or unusable !])]) -AC_CHECK_LIB([config++], +AC_CHECK_LIB([wt], + [main], + , + [AC_MSG_ERROR([libwt not found!])]) + +AC_CHECK_LIB([wtdbo], + [main], + , + [AC_MSG_ERROR([libwtdbo not found!])]) + +AC_CHECK_LIB([wtdbosqlite3], + [main], + , + [AC_MSG_ERROR([libwtdbosqlite3 not found!])]) + +AC_CHECK_LIB([wthttp], [main], , - [AC_MSG_ERROR([config++ not found!])]) + [AC_MSG_ERROR([libwthttp not found!])]) -AC_CHECK_LIB([jpeg], - [jpeg_destroy], - , - [AC_MSG_ERROR([libpthread not found!])]) - -AC_CHECK_LIB([pthread], - [pthread_create], - , - [AC_MSG_ERROR([libpthread not found!])]) AC_CHECK_LIB([avutil], [av_free], @@ -73,16 +64,6 @@ AC_CHECK_LIB([avformat], , [AC_MSG_ERROR([libavformat not found!])]) -AC_CHECK_LIB([ssl], - [SSL_CTX_new], - , - [AC_MSG_ERROR([libssl not found!])]) - -AC_CHECK_LIB([crypto], - [ASN1_STRING_to_UTF8], - , - [AC_MSG_ERROR([libcrypto not found!])]) - AC_CHECK_LIB([boost_system], [main], , @@ -98,58 +79,16 @@ AC_CHECK_LIB( [boost_locale], , [AC_MSG_ERROR([libboost_locale not found!])]) -AC_CHECK_LIB( [boost_thread], - [main], - , - [AC_MSG_ERROR([libboost_thread not found!])]) - AC_CHECK_LIB( [boost_date_time], [main], , [AC_MSG_ERROR([libboost_date_time not found!])]) -#AC_CHECK_LIB( [boost_signals], -# [main], -# , -# [AC_MSG_ERROR([libboost_signals not found!])]) - AC_CHECK_LIB( [boost_iostreams], [main], , [AC_MSG_ERROR([libboost_iostreams not found!])]) -AC_CHECK_LIB( [boost_log], - [main], - , - [AC_MSG_ERROR([libboost_log not found!])]) - -AC_CHECK_LIB( [boost_log_setup], - [main], - , - [AC_MSG_ERROR([libboost_log_setup not found!])]) - -# TODO -> PARAMETRIZE THIS -AC_CHECK_LIB([wthttp], - [main], - , - [AC_MSG_ERROR([libwthttp not found!])]) - -AC_CHECK_LIB([wtdbosqlite3], - [main], - , - [AC_MSG_ERROR([libwtdbosqlite3 not found!])]) - -AC_CHECK_LIB([wtdbo], - [main], - , - [AC_MSG_ERROR([libwtdbo not found!])]) - -AC_CHECK_LIB([wt], - [main], - , - [AC_MSG_ERROR([libwt not found!])]) - - AC_CONFIG_FILES([Makefile src/Makefile test/Makefile]) diff --git a/src/ui/docroot/css/lms.css b/docroot/css/lms.css similarity index 85% rename from src/ui/docroot/css/lms.css rename to docroot/css/lms.css index 025641d4..1bd8c8f4 100644 --- a/src/ui/docroot/css/lms.css +++ b/docroot/css/lms.css @@ -14,31 +14,19 @@ div.contents { padding: 0px 12px 6px; } -.Wt-tableview .playqueue-playing { - color: #FFF; -} - -.Wt-tableview div.playqueue-playing { - background-color: #4A42CA; -} - -.Wt-tableview .Wt-tv-contents.Wt-striped div.playqueue-playing:nth-child(odd) { - background-color: #4A42CA; -} - .playqueue { background-color: #EEE; border-radius: 10px; } .playqueue-playing { - background-color: #4A42CA; + font-weight: bold; } .playqueue-track { + font-size: 120%; margin-top: 12px; line-height: normal; - font-weight: bold; } .playqueue-artist { @@ -70,6 +58,7 @@ div.contents { .mediaplayer-current-track { font-weight: bold; + font-size: 120%; } .mediaplayer-current-artist { diff --git a/src/ui/docroot/images/unknown-cover.jpg b/docroot/images/unknown-cover.jpg similarity index 100% rename from src/ui/docroot/images/unknown-cover.jpg rename to docroot/images/unknown-cover.jpg diff --git a/lms-api/proto/auth.proto b/lms-api/proto/auth.proto deleted file mode 100644 index 1ad02a48..00000000 --- a/lms-api/proto/auth.proto +++ /dev/null @@ -1,65 +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 . - */ - -package LmsAPI; - -message AuthRequest -{ - message Password - { - required string user_login = 1; - required string user_password = 2; - } - - enum Type - { - TypePassword = 1; - } - - required Type type = 1; - - optional Password password = 2; -} - -message AuthResponse -{ - - message PasswordResult - { - enum Type - { - TypePasswordInvalid = 1; - TypeLoginThrottling = 2; // The attempt was not processed because of throttling - TypePasswordValid = 3; - } - required Type type = 1; - - optional uint32 delay = 2; // in seconds, in case of PasswordInvalid/LoginThrottling - } - - enum Type - { - TypePasswordResult = 1; - } - - required Type type = 1; - - optional PasswordResult password_result = 2; -} - diff --git a/lms-api/proto/collection.proto b/lms-api/proto/collection.proto deleted file mode 100644 index 8666a98b..00000000 --- a/lms-api/proto/collection.proto +++ /dev/null @@ -1,195 +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 . - */ - -package LmsAPI; - -message AudioCollectionRequest -{ - - message SearchFilter - { - repeated uint64 artist_id = 1; // has one of these artists - repeated uint64 genre_id = 2; // has one of these genres - repeated uint64 release_id = 3; // hase one of these releases - repeated uint64 track_id = 4; // has one of theses tracks - } - - message BatchParameter - { - required uint32 offset = 1; // First element requested - required uint32 size = 2; // Number of elements requested. Server may not honor this size if too big - } - - message GetGenreList - { - required BatchParameter batch_parameter = 1; - optional SearchFilter search_filter = 2; - } - - message GetArtistList - { - required BatchParameter batch_parameter = 1; - optional SearchFilter search_filter = 2; - } - - message GetReleaseList - { - required BatchParameter batch_parameter = 1; - optional SearchFilter search_filter = 2; - } - - message GetTrackList - { - required BatchParameter batch_parameter = 1; - optional SearchFilter search_filter = 2; - } - - message GetCoverArt - { - enum Type - { - TypeGetCoverArtRelease = 1; - TypeGetCoverArtTrack = 2; - } - - required Type type = 1; - - optional uint64 release_id = 2; // Release that owns the cover art - optional uint64 track_id = 3; // Track that owns the cover art - - required uint32 size = 4; // Scale image to size*size pixels. Set 0 to get the biggest image - } - - enum Type - { - TypeGetRevision = 1; - TypeGetGenreList = 2; - TypeGetArtistList = 3; - TypeGetReleaseList = 4; - TypeGetTrackList = 5; - TypeGetCoverArt = 6; - } - - required Type type = 1; - - optional GetGenreList get_genres = 3; - optional GetArtistList get_artists = 4; - optional GetReleaseList get_releases = 5; - optional GetTrackList get_tracks = 6; - optional GetCoverArt get_cover_art = 7; -} - - -message AudioCollectionResponse -{ - - message Revision - { - required string rev = 1; // Unique identifier of the database revision - } - - message GenreList - { - repeated Genre genres = 1; - } - - message ArtistList - { - repeated Artist artists = 1; - } - - message ReleaseList - { - repeated Release releases = 1; - } - - message TrackList - { - repeated Track tracks = 1; - } - - message CoverArt - { - optional string mime_type = 1; - required bytes data = 2; - } - - message Genre - { - required uint64 id = 1; - optional string mbid = 2; - required string name = 3; - } - - message Artist - { - required uint64 id = 1; - optional string mbid = 2; - required string name = 3; - } - - message Release - { - required uint64 id = 1; - optional string mbid = 2; - required string name = 3; - } - - message Track - { - required uint64 id = 1; // Track id - optional string mbid = 2; - - required uint64 artist_id = 3; - required uint64 release_id = 4; - repeated uint64 genre_id = 5; - - optional string artist_mbid = 6; - optional string release_mbid = 7; - optional uint32 disc_number = 8; - optional uint32 track_number = 9; - required string name = 10; - required uint32 duration_secs = 11; - optional string release_date = 12; - optional string original_release_date = 13; - } - - enum Type { - TypeRevision = 1; - TypeGenreList = 2; - TypeArtistList = 3; - TypeReleaseList = 4; - TypeTrackList = 5; - TypeCoverArt = 6; - } - - - required Type type = 1; - - optional Revision revision = 2; - optional GenreList genre_list = 3; - optional ArtistList artist_list = 4; - optional ReleaseList release_list = 5; - optional TrackList track_list = 6; - repeated CoverArt cover_art = 7; -} - - - - diff --git a/lms-api/proto/media.proto b/lms-api/proto/media.proto deleted file mode 100644 index 20af5d02..00000000 --- a/lms-api/proto/media.proto +++ /dev/null @@ -1,137 +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 . - */ - -package LmsAPI; - -message MediaRequest -{ - - message Prepare - { - - enum AudioCodecType - { - AudioCodecTypeOGA = 1; - } - enum AudioBitrate { - AudioBitrate_32_kbps = 1; - AudioBitrate_64_kbps = 2; - AudioBitrate_96_kbps = 3; - AudioBitrate_128_kbps = 4; - AudioBitrate_192_kbps = 5; - AudioBitrate_256_kbps = 6; - } - - enum VideoCodecType - { - VideoCodecTypeOGV = 1; - } - enum VideoBitrate { - VideoBitrate_512_kbps = 1; - } - - message Audio - { - required int64 track_id = 1; // Id if the media - required AudioCodecType codec_type = 2; - required AudioBitrate bitrate = 3; - optional uint32 stream_idx = 4; - optional uint32 offset_secs = 5; - } - - message Video - { - required int64 video_id = 1; // Id if the media - required VideoCodecType codec_type = 2; - required AudioBitrate audio_bitrate = 3; - required VideoBitrate video_bitrate = 4; - optional uint32 offset_secs = 5; - optional uint32 audio_stream_idx = 6; - optional uint32 video_stream_idx = 7; - optional uint32 subtitle_stream_idx = 8; - } - - enum Type { - AudioRequest = 1; - VideoRequest = 2; - } - - required Type type = 1; - - optional Audio audio = 2; - optional Video video = 3; - } - - message GetPart - { - required uint32 handle = 1; // handle that uniquely identify the media. Get using the Prepare request - required uint32 requested_data_size = 2; // Amount of data requested. May receive more or less. May receive 0 byte if complete - } - - message Terminate - { - required uint32 handle = 1; - } - - enum Type - { - TypeMediaPrepare = 1; - TypeMediaGetPart = 2; - TypeMediaTerminate = 3; - } - - required Type type = 1; - - optional Prepare prepare = 2; - optional GetPart get_part = 3; - optional Terminate terminate = 4; - -} // MediaRequest - -message MediaResponse -{ - - message PrepareResult - { - optional uint32 handle = 1; // handle iif prepare request was ok - } - - message PartResult - { - optional bytes data = 2; // 0 bytes when the media is over, nothing when the request is ill formed - } - - message TerminateResult - { - } - - enum Type - { - TypePrepareResult = 1; - TypePartResult = 2; - TypeTerminateResult = 3; - } - - required Type type = 1; - - optional PrepareResult prepare_result = 2; - optional PartResult part_result = 3; - optional TerminateResult terminate_result = 4; -} - diff --git a/lms-api/proto/messages.proto b/lms-api/proto/messages.proto deleted file mode 100644 index a6eb280b..00000000 --- a/lms-api/proto/messages.proto +++ /dev/null @@ -1,69 +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 . - */ - -import "auth.proto"; -import "collection.proto"; -import "media.proto"; - -package LmsAPI; - -// Every request sent by the client must be a ClientMessage -// The server will answer using a ServerMessage -// -// The client must authenticate first before querying data -// If an expected message is received, the connection is dropped by the server -// -// A header is to be added before the serialized protobuf message in order to keep -// track of where one message ends and the next begins. -// Format: -// MAGIC (4 bytes, 0xDE, 0xAD, 0xBE, 0xEF)- SIZE (4 bytes, big endian encoded) - serialized proto message -message ClientMessage -{ - enum Type - { - AuthRequest = 1; - AudioCollectionRequest = 2; - MediaRequest = 3; - } - - required Type type = 1; - - optional AuthRequest auth_request = 2; - optional AudioCollectionRequest audio_collection_request = 3; - optional MediaRequest media_request = 4; -} - - -message ServerMessage -{ - enum Type - { - AuthResponse = 1; - AudioCollectionResponse = 2; - MediaResponse = 3; - } - - required Type type = 1; - - optional AuthResponse auth_response = 2; - optional AudioCollectionResponse audio_collection_response = 3; - optional MediaResponse media_response = 4; -} - - diff --git a/src/Makefile.am b/src/Makefile.am index e60d4e1c..ce6a0c12 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -2,14 +2,8 @@ bin_PROGRAMS = lms lms_SOURCES = \ $(srcdir)/main/main.cpp \ - $(srcdir)/av/CodecContext.cpp \ - $(srcdir)/av/Common.cpp \ - $(srcdir)/av/Dictionary.cpp \ - $(srcdir)/av/FormatContext.cpp \ - $(srcdir)/av/InputFormatContext.cpp \ - $(srcdir)/av/Stream.cpp \ - $(srcdir)/config/ConfigReader.cpp \ - $(srcdir)/cover/CoverArt.cpp \ + $(srcdir)/av/AvInfo.cpp \ + $(srcdir)/av/AvTranscoder.cpp \ $(srcdir)/cover/CoverArtGrabber.cpp \ $(srcdir)/database/Artist.cpp \ $(srcdir)/database/DatabaseHandler.cpp \ @@ -23,16 +17,12 @@ lms_SOURCES = \ $(srcdir)/database/Video.cpp \ $(srcdir)/database-updater/DatabaseUpdater.cpp \ $(srcdir)/database-updater/Checksum.cpp \ + $(srcdir)/image/Image.cpp \ $(srcdir)/logger/Logger.cpp \ $(srcdir)/metadata/AvFormat.cpp \ $(srcdir)/metadata/Utils.cpp \ $(srcdir)/service/ServiceManager.cpp \ $(srcdir)/service/DatabaseUpdateService.cpp \ - $(srcdir)/service/UserInterfaceService.cpp \ - $(srcdir)/transcode/AvConvTranscoder.cpp \ - $(srcdir)/transcode/Format.cpp \ - $(srcdir)/transcode/Parameters.cpp \ - $(srcdir)/transcode/InputMediaFile.cpp \ $(srcdir)/ui/LmsApplication.cpp \ $(srcdir)/ui/auth/LmsAuth.cpp \ $(srcdir)/ui/audio/desktop/DesktopAudio.cpp \ @@ -51,10 +41,6 @@ lms_SOURCES = \ $(srcdir)/ui/common/LineEdit.cpp \ $(srcdir)/ui/resource/AvConvTranscodeStreamResource.cpp \ $(srcdir)/ui/resource/CoverResource.cpp \ - $(srcdir)/ui/video/VideoWidget.cpp \ - $(srcdir)/ui/video/VideoDatabaseWidget.cpp \ - $(srcdir)/ui/video/VideoMediaPlayerWidget.cpp \ - $(srcdir)/ui/video/VideoParametersDialog.cpp \ $(srcdir)/ui/settings/Settings.cpp \ $(srcdir)/ui/settings/SettingsAccountFormView.cpp \ $(srcdir)/ui/settings/SettingsAudioFormView.cpp \ @@ -65,51 +51,14 @@ lms_SOURCES = \ $(srcdir)/ui/settings/SettingsUserFormView.cpp \ $(srcdir)/ui/settings/SettingsUsers.cpp -if LMSAPI +if VIDEO lms_SOURCES += \ - $(srcdir)/lms-api/server/Connection.cpp \ - $(srcdir)/lms-api/server/ConnectionManager.cpp \ - $(srcdir)/lms-api/server/AudioCollectionRequestHandler.cpp \ - $(srcdir)/lms-api/server/AuthRequestHandler.cpp \ - $(srcdir)/lms-api/server/MediaRequestHandler.cpp \ - $(srcdir)/lms-api/server/RequestHandler.cpp \ - $(srcdir)/lms-api/server/Server.cpp \ - $(srcdir)/service/LmsAPIServerService.cpp - -nodist_lms_SOURCES = \ - $(builddir)/auth.pb.cc \ - $(builddir)/auth.pb.h \ - $(builddir)/collection.pb.cc \ - $(builddir)/collection.pb.h \ - $(builddir)/media.pb.cc \ - $(builddir)/media.pb.h \ - $(builddir)/messages.pb.cc \ - $(builddir)/messages.pb.h - -BUILT_SOURCES = \ - $(builddir)/auth.pb.cc \ - $(builddir)/auth.pb.h \ - $(builddir)/collection.pb.cc \ - $(builddir)/collection.pb.h \ - $(builddir)/media.pb.cc \ - $(builddir)/media.pb.h \ - $(builddir)/messages.pb.cc \ - $(builddir)/messages.pb.h - -MOSTLYCLEANFILES = \ - $(builddir)/auth.pb.cc \ - $(builddir)/auth.pb.h \ - $(builddir)/collection.pb.cc \ - $(builddir)/collection.pb.h \ - $(builddir)/media.pb.cc \ - $(builddir)/media.pb.h \ - $(builddir)/messages.pb.cc \ - $(builddir)/messages.pb.h - -%.pb.cc %.pb.h: $(top_srcdir)/lms-api/proto/%.proto - $(PROTOC) --proto_path=$(top_srcdir)/lms-api/proto/ --cpp_out=$(builddir)/ $^ - + $(srcdir)/ui/video/VideoWidget.cpp \ + $(srcdir)/ui/video/VideoDatabaseWidget.cpp \ + $(srcdir)/ui/video/VideoMediaPlayerWidget.cpp \ + $(srcdir)/ui/video/VideoParametersDialog.cpp endif -lms_CXXFLAGS=-DBOOST_LOG_DYN_LINK -std=c++11 -Wall -I$(top_srcdir)/third-party -I$(srcdir)/ui -I$(srcdir)/lms-api +lms_CXXFLAGS=-std=c++11 -Wall -I$(top_srcdir)/third-party -I$(srcdir)/ui $(MAGICKXX_CFLAGS) +lms_LDADD=$(MAGICKXX_LIBS) diff --git a/src/av/AvInfo.cpp b/src/av/AvInfo.cpp new file mode 100644 index 00000000..ba320c0e --- /dev/null +++ b/src/av/AvInfo.cpp @@ -0,0 +1,306 @@ +/* + * 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 . + */ + +#include + +#include "logger/Logger.hpp" + +#include "AvInfo.hpp" + +namespace Av { + +static std::string streamType_to_string(Stream::Type type) +{ + switch (type) + { + case Stream::Type::Audio: return "audio"; + case Stream::Type::Video: return "video"; + case Stream::Type::Subtitle: return "subtitle"; + } + + return "unknown"; +} + +static std::string averror_to_string(int error) +{ + std::array buf = {0}; + + if (av_strerror(error, buf.data(), buf.size()) == 0) + return std::string(&buf[0]); + else + return "Unknown error"; +} + + +void AvInit() +{ + /* register all the codecs */ + avcodec_register_all(); + av_register_all(); + LMS_LOG(AV, INFO) << "avcodec version = " << avcodec_version(); +} + + + + +MediaFile::MediaFile(const boost::filesystem::path& p) +: _p(p), _context(nullptr) +{ +} + +MediaFile::~MediaFile() +{ + if (_context != nullptr) + avformat_close_input(&_context); +} + +bool +MediaFile::open(void) +{ + if (_context != nullptr) + throw std::logic_error("inputfile '" + _p.string() + "' already open"); + + int error = avformat_open_input(&_context, _p.string().c_str(), nullptr, nullptr); + if (error < 0) + { + LMS_LOG(AV, ERROR) << "Cannot open '" << _p.string() << "': " << averror_to_string(error); + return false; + } + + return true; +} + +bool +MediaFile::scan(void) +{ + if (_context == nullptr) + throw std::logic_error("inputfile '" + _p.string() + "' not open"); + + int error = avformat_find_stream_info(_context, nullptr); + if (error < 0) + { + LMS_LOG(AV, ERROR) << "Cannot find stream information on '" << _p.string() << "': " << averror_to_string(error); + return false; + } + + return true; +} + +boost::posix_time::time_duration +MediaFile::getDuration() const +{ + if (_context == nullptr) + throw std::logic_error("inputfile '" + _p.string() + "' not open"); + + if (static_cast(_context->duration) != AV_NOPTS_VALUE ) + return boost::posix_time::seconds(_context->duration / AV_TIME_BASE); + else + return boost::posix_time::seconds(0); // TODO, do something better? +} + +void +getMetaDataFromDictionnary(AVDictionary* dictionnary, std::map& res) +{ + if (!dictionnary) + return; + + AVDictionaryEntry *tag = NULL; + while ((tag = av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX))) + { + res.insert( std::make_pair(tag->key, tag->value)); + } +} + +std::map +MediaFile::getMetaData(void) +{ + if (_context == nullptr) + throw std::logic_error("inputfile '" + _p.string() + "' not open"); + + std::map res; + + getMetaDataFromDictionnary(_context->metadata, res); + + // HACK for OGG files + // If we did not find tags, search metadata in streams + if (res.empty()) + { + for (std::size_t i = 0; i < _context->nb_streams; ++i) + { + getMetaDataFromDictionnary(_context->streams[i]->metadata, res); + + if (!res.empty()) + break; + } + } + + return res; +} + +std::vector +MediaFile::getStreams(Stream::Type type) const +{ + if (_context == nullptr) + throw std::logic_error("inputfile '" + _p.string() + "' not open"); + + std::vector res; + + for (std::size_t i = 0; i < _context->nb_streams; ++i) + { + AVStream* avstream = _context->streams[i]; + + // Skip attached pics + if (avstream->disposition & AV_DISPOSITION_ATTACHED_PIC) + continue; + + if (avstream->codec == nullptr) + { + LMS_LOG(AV, ERROR) << "Skipping stream " << i << " since no codec is set"; + continue; + } + + if (type == Stream::Type::Audio && avstream->codec->codec_type != AVMEDIA_TYPE_AUDIO) + continue; + else if (type == Stream::Type::Video && avstream->codec->codec_type != AVMEDIA_TYPE_VIDEO) + continue; + else if (type == Stream::Type::Subtitle && avstream->codec->codec_type != AVMEDIA_TYPE_SUBTITLE) + continue; + + Stream stream; + + stream.id = i; // or use stream->id ? + stream.type = type; + stream.bitrate = avstream->codec->bit_rate; + + { + std::array buf = {0}; + avcodec_string(buf.data(), buf.size(), avstream->codec, 0); + + stream.desc = buf.data(); + } + + res.push_back(stream); + } + + return res; +} + +int +MediaFile::getBestStreamId(Stream::Type type) const +{ + if (_context == nullptr) + throw std::logic_error("inputfile '" + _p.string() + "' not open"); + + enum AVMediaType avMediaType; + + switch (type) + { + case Stream::Type::Audio: avMediaType = AVMEDIA_TYPE_AUDIO; break; + case Stream::Type::Video: avMediaType = AVMEDIA_TYPE_VIDEO; break; + case Stream::Type::Subtitle: avMediaType = AVMEDIA_TYPE_SUBTITLE; break; + default: + return -1; + } + + int res = av_find_best_stream(_context, + avMediaType, + -1, // Auto + -1, // Auto + NULL, + 0); + if (res < 0) + { + LMS_LOG(AV, ERROR) << "Cannot find best stream for type " << streamType_to_string(type); + return -1; + } + + return res; +} + +bool +MediaFile::hasAttachedPictures(void) const +{ + for (std::size_t i = 0; i < _context->nb_streams; ++i) + { + if (_context->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC) + return true; + } + + return false; +} + +std::vector +MediaFile::getAttachedPictures(std::size_t nbMaxPictures) const +{ + if (_context == nullptr) + throw std::logic_error("inputfile '" + _p.string() + "' not open"); + + static const std::map codecMimeMap = + { + { AV_CODEC_ID_BMP, "image/x-bmp" }, + { AV_CODEC_ID_GIF, "image/gif" }, + { AV_CODEC_ID_MJPEG, "image/jpeg" }, + { AV_CODEC_ID_PNG, "image/png" }, + { AV_CODEC_ID_PNG, "image/x-png" }, + { AV_CODEC_ID_PPM, "image/x-portable-pixmap" }, + }; + + std::vector pictures; + + for (std::size_t i = 0; i < _context->nb_streams; ++i) + { + AVStream *avstream = _context->streams[i]; + + // Skip attached pics + if (!(avstream->disposition & AV_DISPOSITION_ATTACHED_PIC)) + continue; + + if (avstream->codec == nullptr) + { + LMS_LOG(AV, ERROR) << "Skipping stream " << i << " since no codec is set"; + continue; + } + + Picture picture; + + auto itMime = codecMimeMap.find(avstream->codec->codec_id); + if (itMime != codecMimeMap.end()) + { + picture.mimeType = itMime->second; + } + else + { + picture.mimeType = "application/octet-stream"; + LMS_LOG(AV, ERROR) << "CODEC ID " << avstream->codec->codec_id << " not handled in mime type conversion"; + } + + AVPacket pkt = avstream->attached_pic; + + std::copy(pkt.data, pkt.data + pkt.size, std::back_inserter(picture.data)); + + pictures.push_back( picture ); + + if (pictures.size() >= nbMaxPictures) + break; + } + + return pictures; +} + +} // namespace Av diff --git a/src/av/AvInfo.hpp b/src/av/AvInfo.hpp new file mode 100644 index 00000000..377fd6fd --- /dev/null +++ b/src/av/AvInfo.hpp @@ -0,0 +1,102 @@ +/* + * 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 . + */ + +/* This file contains some classes in order to get info from file using the libavconv */ + +#ifndef AV_INFO_HPP +#define AV_INFO_HPP + +extern "C" +{ +#define __STDC_CONSTANT_MACROS +#include +#include +#include +} + +#include +#include +#include +#include + +#include +#include //no i/o just types + +namespace Av +{ + +void AvInit(); + +struct Picture +{ + std::string mimeType; + std::vector data; +}; + +struct Stream +{ + enum class Type + { + Audio, + Video, + Subtitle, + }; + + int id; + Type type; + std::size_t bitrate; + std::string desc; // Description of the stream +}; + +class MediaFile +{ + public: + + MediaFile(const boost::filesystem::path& p); + ~MediaFile(); + + // non copyable + MediaFile(const MediaFile&) = delete; + MediaFile& operator=(const MediaFile&) = delete; + + boost::filesystem::path getPath() const {return _p;}; + + bool open(void); + bool scan(void); + + boost::posix_time::time_duration getDuration() const; + std::map getMetaData(void); + + std::vector getStreams(Stream::Type type) const; + int getBestStreamId(Stream::Type type) const; // -1 if failure/unknown + bool hasAttachedPictures(void) const; + std::vector getAttachedPictures(std::size_t nbMaxPictures) const; + + private: + MediaFile(); + + boost::filesystem::path _p; + AVFormatContext* _context; +}; + + +} // namespace Av + +#endif + diff --git a/src/av/AvTranscoder.cpp b/src/av/AvTranscoder.cpp new file mode 100644 index 00000000..a9d8171a --- /dev/null +++ b/src/av/AvTranscoder.cpp @@ -0,0 +1,330 @@ +/* + * 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 . + */ +#include +#include + +#include + +#include "logger/Logger.hpp" + +#include "AvTranscoder.hpp" + +namespace Av { + +#define LMS_LOG_TRANSCODE(sev) LMS_LOG(TRANSCODE, INFO) << "[" << _id << "] - " + +std::string encoding_to_mimetype(Encoding encoding) +{ + switch(encoding) + { + case Encoding::MP3: return "audio/mp3"; + case Encoding::OGA: return "audio/ogg"; + 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 ""; +} + +// TODO, parametrize? +static const std::vector execNames = +{ + "avconv", + "ffmpeg", +}; + +static std::mutex transcoderMutex; +static boost::filesystem::path avConvPath = boost::filesystem::path(); +static std::atomic globalId = {0}; + +static std::string searchPath(std::string filename) +{ + std::string path; + + path = ::getenv("PATH"); + if (path.empty()) + throw std::runtime_error("Environment variable PATH not found"); + + std::string result; + typedef boost::tokenizer > tokenizer; + boost::char_separator sep(":"); + tokenizer tok(path, sep); + for (tokenizer::iterator it = tok.begin(); it != tok.end(); ++it) + { + boost::filesystem::path p = *it; + p /= filename; + if (!::access(p.c_str(), X_OK)) + { + result = p.string(); + break; + } + } + return result; +} + +void +Transcoder::init() +{ + for (std::string execName : execNames) + { + boost::filesystem::path p = searchPath(execName); + if (!p.empty()) + { + avConvPath = p; + break; + } + } + + if (!avConvPath.empty()) + LMS_LOG(TRANSCODE, INFO) << "Using transcoder " << avConvPath; + else + throw std::runtime_error("Cannot find any transcoder binary!"); +} + +Transcoder::Transcoder(boost::filesystem::path filePath, TranscodeParameters parameters) +: _filePath(filePath), + _parameters(parameters), + _isComplete(false), + _id(globalId++) +{ + +} + +bool +Transcoder::start() +{ + if (!boost::filesystem::exists(_filePath)) + return false; + else if (!boost::filesystem::is_regular( _filePath) ) + return false; + + LMS_LOG_TRANSCODE(INFO) << "Transcoding file '" << _filePath << "'"; + + std::vector args; + + args.push_back(avConvPath.string()); + + // Make sure we do not produce anything in the stderr output + // in order not to block the whole forked process + args.push_back("-loglevel"); + args.push_back("quiet"); + + // input Offset + if (_parameters.getOffset().total_seconds() > 0) + { + args.push_back("-ss"); + args.push_back(std::to_string(_parameters.getOffset().total_seconds())); + } + + // Input file + args.push_back("-i"); + args.push_back(_filePath.string()); + + // Output bitrates + args.push_back("-b:a"); + args.push_back(std::to_string(_parameters.getBitrate(Stream::Type::Audio))); +// if (_parameters.getOutputFormat().getType() == Format::Video) +// oss << " -b:v " << _parameters.getOutputBitrate(Stream::Video); + + // Stream mapping + for (int streamId : _parameters.getSelectedStreamIds()) + { + // 0 means the first input file + args.push_back("-map"); + args.push_back("0:" + std::to_string(streamId)); + } + + // Codecs and formats + switch( _parameters.getEncoding()) + { + case Encoding::MP3: + args.push_back("-f"); + args.push_back("mp3"); + break; + + case Encoding::OGA: + args.push_back("-acodec"); + args.push_back("libvorbis"); + args.push_back("-f"); + args.push_back("ogg"); + break; + + case Encoding::OGV: + args.push_back("-acodec"); + args.push_back("libvorbis"); + args.push_back("-ac"); + args.push_back("2"); + args.push_back("-ar"); + args.push_back("44100"); + args.push_back("-vcodec"); + args.push_back("libtheora"); + args.push_back("-threads"); + args.push_back("4"); + args.push_back("-f"); + args.push_back("ogg"); + break; + case Encoding::WEBMA: + args.push_back("-codec:a"); + args.push_back("libvorbis"); + args.push_back("-f"); + args.push_back("webm"); + break; + + case Encoding::WEBMV: + args.push_back("-acodec"); + args.push_back("libvorbis"); + args.push_back("-ac"); + args.push_back("2"); + args.push_back("-ar"); + args.push_back("44100"); + args.push_back("-vcodec"); + args.push_back("libvpx"); + args.push_back("-threads"); + args.push_back("4"); + args.push_back("-f"); + args.push_back("webm"); + break; + + case Encoding::M4A: + args.push_back("-acodec"); + args.push_back("aac"); + args.push_back("-f"); + args.push_back("mp4"); + args.push_back("-strict"); + args.push_back("experimental"); + break; + + case Encoding::M4V: + args.push_back("-acodec"); + args.push_back("aac"); + args.push_back("-strict"); + args.push_back("experimental"); + args.push_back("-ac"); + args.push_back("2"); + args.push_back("-ar"); + args.push_back("-44100"); + args.push_back("-vcodec"); + args.push_back("libx264"); + args.push_back("-f"); + args.push_back("m4v"); + break; + + 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: + return false; + } + + args.push_back("pipe:1"); + + LMS_LOG_TRANSCODE(INFO) << "Dumping args (" << args.size() << ")"; + for (std::string arg : args) + LMS_LOG_TRANSCODE(DEBUG) << "Arg = '" << arg << "'"; + + // make sure only one thread is executing this part of code + { + std::lock_guard lock(transcoderMutex); + + _child = std::make_shared(); + + const redi::pstreams::pmode mode = redi::pstreams::pstdout; // | redi::pstreams::pstderr; + _child->open(avConvPath.string(), args, mode); + if (!_child->is_open()) + { + LMS_LOG_TRANSCODE(DEBUG) << "Exec failed!"; + return false; + } + + if (_child->out().eof()) + { + LMS_LOG_TRANSCODE(DEBUG) << "Early end of file!"; + return false; + } + + LMS_LOG_TRANSCODE(DEBUG) << "Stream opened!"; + } + + return true; +} + +void +Transcoder::process(std::vector& output, std::size_t maxSize) +{ + if (!_child || _isComplete) + return; + + output.resize(maxSize); + + //Read on the output stream + _child->out().read(reinterpret_cast(&output[0]), maxSize); + output.resize(_child->out().gcount()); + + LMS_LOG_TRANSCODE(DEBUG) << "Read " << output.size() << " bytes"; + + if (_child->out().eof()) + { + LMS_LOG_TRANSCODE(DEBUG) << "Stdout EOF!"; + _child->clear(); + + _isComplete = true; + _child.reset(); + } + + _total += output.size(); + + LMS_LOG_TRANSCODE(DEBUG) << "nb bytes = " << output.size() << ", total = " << _total; +} + +Transcoder::~Transcoder() +{ + LMS_LOG_TRANSCODE(DEBUG) << ", ~Transcoder called! Total produced bytes = " << _total; + + if (_child) + { + LMS_LOG_TRANSCODE(DEBUG) << "Child still here!"; + _child->rdbuf()->kill(SIGKILL); + LMS_LOG_TRANSCODE(DEBUG) << "Closing..."; + _child->rdbuf()->close(); + LMS_LOG_TRANSCODE(DEBUG) << "Closing DONE"; + } +} + +} // namespace Transcode diff --git a/src/av/AvTranscoder.hpp b/src/av/AvTranscoder.hpp new file mode 100644 index 00000000..3f7a71f7 --- /dev/null +++ b/src/av/AvTranscoder.hpp @@ -0,0 +1,108 @@ +/* + * 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 . + */ + +#pragma once + +#include +#include + +#include "pstreams/pstream.h" + +#include //no i/o just types +#include +#include + +#include "AvInfo.hpp" + +namespace Av { + + +enum class Encoding +{ + OGA, + OGV, + MP3, + WEBMA, + WEBMV, + FLA, + FLV, + M4A, + M4V, +}; + +std::string encoding_to_mimetype(Encoding encoding); + +class TranscodeParameters +{ + public: + + // Setters + void setEncoding(Encoding encoding) { _encoding = encoding; } + void setOffset(boost::posix_time::time_duration offset) {_offset = offset; } + void setBitrate(Stream::Type type, std::size_t bitrate) { _outputBitrate[type] = bitrate; } + + // Manually add the streams to be transcoded + // If no stream is added, input streams are selected automatically + void addStream(int inputStreamId) { _selectedStreams.insert(inputStreamId); } + + // Getters + Encoding getEncoding(void) const { return _encoding; } + boost::posix_time::time_duration getOffset(void) const { return _offset; } + std::set getSelectedStreamIds(void) const { return _selectedStreams; } + std::size_t getBitrate(Stream::Type type) { return _outputBitrate[type]; } + + private: + Encoding _encoding = Encoding::MP3; + boost::posix_time::time_duration _offset = boost::posix_time::seconds(0); + std::set _selectedStreams; + std::map _outputBitrate = { {Stream::Type::Audio, 0}, { Stream::Type::Video, 0}, { Stream::Type::Subtitle, 0} }; +}; + + +class Transcoder +{ + public: + static void init(); + + Transcoder(boost::filesystem::path file, TranscodeParameters parameters); + ~Transcoder(); + + // non copyable + Transcoder(const Transcoder&) = delete; + Transcoder& operator=(const Transcoder&) = delete; + + bool start(); + void process(std::vector& output, std::size_t maxSize); + bool isComplete(void) { return _isComplete; } + + private: + Transcoder(); + + boost::filesystem::path _filePath; + TranscodeParameters _parameters; + + std::shared_ptr _child; + + bool _isComplete = false; + std::size_t _total = 0; + std::size_t _id; +}; + +} // namespace Av + diff --git a/src/av/Codec.cpp b/src/av/Codec.cpp deleted file mode 100644 index d5e6e7e3..00000000 --- a/src/av/Codec.cpp +++ /dev/null @@ -1,76 +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 . - */ - -#include -#include -#include - -#include "logger/Logger.hpp" - -#include "Codec.hpp" - -Codec::Codec(enum CodecID codec, Type type ) -: _codec(nullptr) -{ - if (type == Encoder) - _codec = avcodec_find_encoder(codec); - else if (type == Decoder) - _codec = avcodec_find_decoder(codec); - - if (_codec == nullptr) { - LMS_LOG(MOD_AV, SEV_ERROR) << "Codec constructor failed! codec = " << codec << ", type = " << type; - throw std::runtime_error("can't find codec using this id!"); - } -} - -Codec::Codec(const AVCodec* codec) -: _codec(codec) -{ - assert(_codec != nullptr); -} - -Codec::~Codec() -{ - if (_codec == nullptr) { - // TODO release iif ownership has not been taken? -// av_codec_close(_codec); - } -} - -const AVCodec* -Codec::get() const -{ - assert(_codec != nullptr); - return _codec; -} - -Codec::Id -Codec::getId() const -{ - assert(_codec != nullptr); - return _codec->id; -} - -std::string -Codec::getName() const -{ - assert(_codec != nullptr); - return _codec->name; -} - diff --git a/src/av/Codec.hpp b/src/av/Codec.hpp deleted file mode 100644 index 94220355..00000000 --- a/src/av/Codec.hpp +++ /dev/null @@ -1,56 +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 . - */ - -#ifndef CODEC_HPP__ -#define CODEC_HPP__ - -#include -#include -#include - -#include "Common.hpp" - -class Codec : boost::noncopyable -{ - friend class CodecContext; - friend class FormatContext; - friend class OutputFormatContext; - - public: - - typedef enum AVCodecID Id; - enum Type { - Encoder, - Decoder, - }; - - Codec(Id codecId, Type type); - Codec(const AVCodec* codec); // Attach existing codec - ~Codec(); - - Id getId() const; - std::string getName() const; - - private: - - const AVCodec* get() const; - const AVCodec* _codec; -}; - -#endif diff --git a/src/av/CodecContext.cpp b/src/av/CodecContext.cpp deleted file mode 100644 index 1f9ee44c..00000000 --- a/src/av/CodecContext.cpp +++ /dev/null @@ -1,54 +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 . - */ - -#include -#include -#include -#include - -#include "CodecContext.hpp" - -namespace Av -{ - -CodecContext::CodecContext(AVCodecContext* CodecContext) -: _codecContext(CodecContext) -{ - assert(_codecContext != nullptr); -} - - -void -CodecContext::dumpInfo(std::ostream& ost) const -{ - ost << "BitRate = " << getBitRate() << ", SampleFormat = " << getSampleFormat() << ", SampleRate = " << getSampleRate() << ", ChannelLayout = " << getChannelLayout() << ", NbChannels = " << getNbChannels() << ", Timebase = " << getTimeBase().num << "/" << getTimeBase().den; -} - - -std::string -CodecContext::getCodecDesc(void) const -{ - std::array buf; - avcodec_string(buf.data(), buf.size(), _codecContext, 0); - - return std::string(buf.data()); -} - -} // namespace Av - diff --git a/src/av/CodecContext.hpp b/src/av/CodecContext.hpp deleted file mode 100644 index 4796a540..00000000 --- a/src/av/CodecContext.hpp +++ /dev/null @@ -1,63 +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 . - */ - -#ifndef CODEC_CONTEXT_HPP -#define CODEC_CONTEXT_HPP - -#include -#include - -#include "Codec.hpp" - -namespace Av -{ - -class CodecContext -{ - public: - - CodecContext(AVCodecContext* CodecContext); // Attach existing codec context (no free will be done) - -// Codec getCodec(); - - enum AVMediaType getType(void) const { return _codecContext->codec_type; } - Codec::Id getCodecId(void) const { return _codecContext->codec_id; } - - std::string getCodecDesc(void) const; - - // Accessors - std::size_t getBitRate() const {return _codecContext->bit_rate;} - AVSampleFormat getSampleFormat() const {return _codecContext->sample_fmt;} - std::size_t getSampleRate() const {return _codecContext->sample_rate;} - std::uint64_t getChannelLayout() const {return _codecContext->channel_layout;} - std::size_t getNbChannels() const {return _codecContext->channels; } - AVRational getTimeBase() const {return _codecContext->time_base; } - - void dumpInfo(std::ostream& ost) const; - - private: - AVCodecContext* native() { return _codecContext; } - - AVCodecContext* _codecContext; -}; - -} // namespace Av - -#endif - diff --git a/src/av/Common.cpp b/src/av/Common.cpp deleted file mode 100644 index 24778fa9..00000000 --- a/src/av/Common.cpp +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2013 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#include - -#include "logger/Logger.hpp" - -#include "Common.hpp" - -namespace Av -{ - -std::string -AvError::to_str(void) const -{ - boost::array buf = {0}; - if (av_strerror(_errnum, buf.data(), buf.size()) == 0) - return std::string(&buf[0]); - else - return "Unknown error"; - -} - - -std::ostream& operator<<(std::ostream& ost, const AvError& err) -{ - ost << err.to_str(); - return ost; -} - -void AvInit() -{ - /* register all the codecs */ - avcodec_register_all(); - av_register_all(); - LMS_LOG(MOD_AV, SEV_INFO) << "avcodec version = " << avcodec_version(); -} - -} // namespace Av diff --git a/src/av/Common.hpp b/src/av/Common.hpp deleted file mode 100644 index ddb1b17b..00000000 --- a/src/av/Common.hpp +++ /dev/null @@ -1,67 +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 . - */ - -#ifndef TRANSCODE_COMMON_HPP__ -#define TRANSCODE_COMMON_HPP__ - -// Hack to properly iinclude libavcodec/avcodec.h... -// -#define __STDC_CONSTANT_MACROS -#ifdef _STDINT_H -#undef _STDINT_H -#endif -# include -extern "C" { -#include -#include -#include -#include -} - -#include - -namespace Av -{ - - -void AvInit(); - -class AvError -{ - public: - AvError() : _errnum(0) {} - AvError(int ernum) : _errnum(ernum) {} - - void operator=(int errnum) { _errnum = errnum; } - - operator bool() { return _errnum < 0; } - - std::string to_str() const; - - friend std::ostream& operator<<(std::ostream& ost, const AvError&); - - bool eof() { return _errnum == AVERROR_EOF; } // TODO - - private: - int _errnum; -}; - -} // namespace Av - -#endif diff --git a/src/av/Dictionary.cpp b/src/av/Dictionary.cpp deleted file mode 100644 index 0466e351..00000000 --- a/src/av/Dictionary.cpp +++ /dev/null @@ -1,50 +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 . - */ - -#include "Dictionary.hpp" - - -namespace Av -{ - -Dictionary::Dictionary(AVDictionary* dictionary) -: _dictionary(dictionary) -{ -} - -void -Dictionary::get(std::map& entries) -{ - AVDictionaryEntry *tag = NULL; - while ((tag = av_dict_get(_dictionary, "", tag, AV_DICT_IGNORE_SUFFIX))) { - entries.insert( std::make_pair(tag->key, tag->value)); - } -} - -std::string -Dictionary::get(std::string key) -{ - AVDictionaryEntry *tag = NULL; - tag = av_dict_get(_dictionary, key.c_str(), tag, 0); - - return tag != NULL ? std::string(tag->value) : std::string(); -} - -} // namespace Av - diff --git a/src/av/Dictionary.hpp b/src/av/Dictionary.hpp deleted file mode 100644 index cbb9f70c..00000000 --- a/src/av/Dictionary.hpp +++ /dev/null @@ -1,47 +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 . - */ - -#ifndef AV_DICTIONARY_HPP -#define AV_DICTIONARY_HPP - -#include -#include "Common.hpp" - -namespace Av -{ - -class Dictionary -{ - public: - Dictionary(AVDictionary* dictionary); - - // Get all - void get(std::map& entries); - - // get a single entry - std::string get(std::string key); - - private: - AVDictionary* _dictionary; -}; - -} // namespace Av - -#endif - diff --git a/src/av/FormatContext.cpp b/src/av/FormatContext.cpp deleted file mode 100644 index 56a52c7f..00000000 --- a/src/av/FormatContext.cpp +++ /dev/null @@ -1,35 +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 . - */ - - -#include "FormatContext.hpp" - -namespace Av -{ - -FormatContext::FormatContext() -: _context(nullptr) -{ -} - -FormatContext::~FormatContext() -{ -} - -} // namespace Av diff --git a/src/av/FormatContext.hpp b/src/av/FormatContext.hpp deleted file mode 100644 index 535a1f24..00000000 --- a/src/av/FormatContext.hpp +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2013 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#ifndef FORMAT_CONTEXT_HPP -#define FORMAT_CONTEXT_HPP - -#include - -#include "Common.hpp" - -namespace Av -{ - -class FormatContext -{ - public: - FormatContext(); - ~FormatContext(); - - - protected: - void native(AVFormatContext* c) { _context = c;} - AVFormatContext* native() { return _context; } - const AVFormatContext* native() const { return _context; } - - private: - - AVFormatContext* _context; - - -}; - -} // namespace Av - -#endif - diff --git a/src/av/InputFormatContext.cpp b/src/av/InputFormatContext.cpp deleted file mode 100644 index e455d9e7..00000000 --- a/src/av/InputFormatContext.cpp +++ /dev/null @@ -1,173 +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 . - */ - -#include -#include - -#include "logger/Logger.hpp" - -#include "InputFormatContext.hpp" - -namespace Av -{ - -InputFormatContext::InputFormatContext(const boost::filesystem::path& p) -: _path(p) -{ - AVFormatContext* context = nullptr; - - // The last three parameters specify the file format, buffer size and - // format parameters. By simply specifying NULL or 0 we ask libavformat - // to auto-detect the format and use a default buffer size. - AvError error = avformat_open_input(&context, p.string().c_str(), nullptr, nullptr); - if (error) - { - LMS_LOG(MOD_AV, SEV_ERROR) << "Cannot open '" << p.string() << "', avformat_open_input returned " << error; - throw std::runtime_error("avformat_open_input failed: " + error.to_str()); - } - - native(context); - -} - -InputFormatContext::~InputFormatContext() -{ - AVFormatContext* context = native(); - avformat_close_input(&context); -} - -std::vector -InputFormatContext::getStreams(void) -{ - std::vector res; - - for (std::size_t i = 0; i < native()->nb_streams; ++i) { - res.push_back( Stream(native()->streams[i])); - } - - return res; -} - - -bool -InputFormatContext::getBestStreamIdx(AVMediaType type, Stream::Idx& index) -{ - int res = av_find_best_stream(native(), - type, - -1, // Auto - -1, // Auto - NULL, - 0 - ); - - AvError error(res); - if (error) { - LMS_LOG(MOD_AV, SEV_DEBUG) << "Cannot get best stream for type " << type << ": " << error; - return false; - } - else { - index = res; - return true; - } -} - -void -InputFormatContext::findStreamInfo(void) -{ - AvError err = avformat_find_stream_info(native(), NULL); - if (err) { - LMS_LOG(MOD_AV, SEV_ERROR) << "Couldn't find stream information: " << err; - throw std::runtime_error("av_find_stream_info failed!"); - } -} - - -std::size_t -InputFormatContext::getDurationSecs() const -{ - if (static_cast(native()->duration) != AV_NOPTS_VALUE ) - return native()->duration / AV_TIME_BASE; - else - return 0; // TODO, do something better? -} - -Dictionary -InputFormatContext::getMetadata(void) -{ - return Dictionary(native()->metadata); - -} - -std::size_t -InputFormatContext::getNbPictures(void) const -{ - std::size_t res = 0; - - for (std::size_t i = 0; i < native()->nb_streams; ++i) - { - if (native()->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC) - ++res; - } - return res; -} - -std::vector -InputFormatContext::getPictures(std::size_t nbMaxPictures) const -{ - static const std::map codecMimeMap = - { - { AV_CODEC_ID_BMP, "image/x-bmp" }, - { AV_CODEC_ID_GIF, "image/gif" }, - { AV_CODEC_ID_MJPEG, "image/jpeg" }, - { AV_CODEC_ID_PNG, "image/png" }, - { AV_CODEC_ID_PNG, "image/x-png" }, - { AV_CODEC_ID_PPM, "image/x-portable-pixmap" }, - }; - - std::vector pictures; - - for (std::size_t i = 0; i < native()->nb_streams; ++i) - { - Stream stream(native()->streams[i]); - - if (stream.hasAttachedPic()) - { - Picture picture; - - auto itMime = codecMimeMap.find(stream.getCodecContext().getCodecId()); - if (itMime != codecMimeMap.end()) - picture.mimeType = itMime->second; - else - picture.mimeType = "application/octet-stream"; - - AVPacket pkt = native()->streams[i]->attached_pic; - - std::copy(pkt.data, pkt.data + pkt.size, std::back_inserter(picture.data)); - - pictures.push_back( picture ); - - if (pictures.size() >= nbMaxPictures) - break; - } - } - - return pictures; -} - -} //namespace Av diff --git a/src/av/InputFormatContext.hpp b/src/av/InputFormatContext.hpp deleted file mode 100644 index 9385fdcc..00000000 --- a/src/av/InputFormatContext.hpp +++ /dev/null @@ -1,72 +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 . - */ - -#ifndef INPUT_FORMAT_CONTEXT_HPP -#define INPUT_FORMAT_CONTEXT_HPP - -#include - -#include - -#include "FormatContext.hpp" -#include "Stream.hpp" -#include "Dictionary.hpp" - -namespace Av -{ - -struct Picture { - std::string mimeType; - std::vector data; -}; - -class InputFormatContext : public FormatContext -{ - public: - - InputFormatContext(const boost::filesystem::path& p); - ~InputFormatContext(); - - - Dictionary getMetadata(void); // metadata access - - // Scan file - void findStreamInfo(); - - - // Get attached pictures - std::size_t getNbPictures(void) const; - std::vector getPictures(std::size_t nbMaxPictures) const; - - // Get the streams - std::vector getStreams(void); - - bool getBestStreamIdx(enum AVMediaType type, Stream::Idx& idx); - std::size_t getDurationSecs() const; - - private: - boost::filesystem::path _path; - - -}; - -} // namespace av - -#endif - diff --git a/src/av/Stream.cpp b/src/av/Stream.cpp deleted file mode 100644 index 4a6875f0..00000000 --- a/src/av/Stream.cpp +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (C) 2013 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#include -#include "Stream.hpp" - -namespace Av -{ - -Stream::Stream(AVStream* stream) -: _stream(stream) -{ - assert(_stream != nullptr); - assert(_stream->codec != nullptr); -} - -CodecContext -Stream::getCodecContext() -{ - return _stream->codec; -} - -Dictionary -Stream::getMetadata(void) -{ - return Dictionary(_stream->metadata); -} - -bool -Stream::hasAttachedPic(void) const -{ - return (_stream->disposition & AV_DISPOSITION_ATTACHED_PIC); -} - -} // namespace Av diff --git a/src/av/Stream.hpp b/src/av/Stream.hpp deleted file mode 100644 index 20caab96..00000000 --- a/src/av/Stream.hpp +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2013 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#ifndef STREAM_HPP__ -#define STREAM_HPP__ - -#include "Common.hpp" - -#include "CodecContext.hpp" -#include "Dictionary.hpp" - -namespace Av -{ - -class Stream -{ - friend class InputFormatContext; - - public: - // Attach existing stream - Stream(AVStream* stream); - - typedef size_t Idx; - -// Idx getIdx() const { return _stream->index; } - - bool hasAttachedPic(void) const; - Dictionary getMetadata(void); - CodecContext getCodecContext(void); - - private: - - AVStream* _stream; -}; - -} // namespace Av - -#endif - diff --git a/src/config/.empty b/src/config/.empty new file mode 100644 index 00000000..e69de29b diff --git a/src/config/ConfigReader.cpp b/src/config/ConfigReader.cpp deleted file mode 100644 index 90c093de..00000000 --- a/src/config/ConfigReader.cpp +++ /dev/null @@ -1,74 +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 . - */ - -#include - -#include "ConfigReader.hpp" - -namespace { - -} - -ConfigReader::ConfigReader() -: _config (nullptr) -{ -} - -ConfigReader& -ConfigReader::instance() -{ - static ConfigReader instance; - return instance; -} - -void -ConfigReader::setFile(boost::filesystem::path p) -{ - if (_config != nullptr) - delete _config; - - _config = new libconfig::Config(); - - _config->readFile(p.string().c_str()); -} - -std::string -ConfigReader::getString(std::string setting) -{ - return _config->lookup(setting); -} - -unsigned long -ConfigReader::getULong(std::string setting) -{ - return static_cast(_config->lookup(setting)); -} - -long -ConfigReader::getLong(std::string setting) -{ - return _config->lookup(setting); -} - -bool -ConfigReader::getBool(std::string setting) -{ - return _config->lookup(setting); -} - diff --git a/src/config/ConfigReader.hpp b/src/config/ConfigReader.hpp deleted file mode 100644 index 6f68156c..00000000 --- a/src/config/ConfigReader.hpp +++ /dev/null @@ -1,49 +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 . - */ - -#ifndef CONFIG_READER_HPP -#define CONFIG_READER_HPP - -#include -#include - -class ConfigReader -{ - public: - - ConfigReader(const ConfigReader&) = delete; - ConfigReader& operator=(const ConfigReader&) = delete; - - static ConfigReader& instance(); - - void setFile(boost::filesystem::path p); - - std::string getString(std::string setting); - unsigned long getULong(std::string setting); - long getLong(std::string setting); - bool getBool(std::string setting); - - private: - - ConfigReader(); - - libconfig::Config *_config; -}; - -#endif diff --git a/src/cover/CoverArt.cpp b/src/cover/CoverArt.cpp deleted file mode 100644 index 35b2c991..00000000 --- a/src/cover/CoverArt.cpp +++ /dev/null @@ -1,80 +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 . - */ - -#include -#include -#include -#include -#include -#include - -#include "logger/Logger.hpp" - -#include "CoverArt.hpp" - -namespace CoverArt { - -bool -CoverArt::scale(std::size_t size) -{ - bool res = false; - - if (!size) - return false; - - try { - - boost::gil::rgb8_image_t source; - boost::gil::rgb8_image_t dest(size, size); - - // Read source - { - std::istringstream iss( std::string(_data.begin(), _data.end())); - boost::gil::read_image(iss, source, boost::gil::jpeg_tag()); - } - - if (source.width() == static_cast(size) - && source.height() == static_cast(size)) - return true; - - // Resize - boost::gil::resize_view(boost::gil::const_view(source), - boost::gil::view(dest), - boost::gil::bilinear_sampler()); - - // Output to dest - { - std::ostringstream oss; - boost::gil::write_view(oss, boost::gil::const_view(dest), boost::gil::jpeg_tag()); - - std::string output = oss.str(); - _data.assign(output.begin(), output.end()); - } - - res = true; - } - catch(std::exception& e) - { - LMS_LOG(MOD_COVER, SEV_ERROR) << "Caught exception: " << e.what(); - } - - return res; -} - -} // namespace CoverArt diff --git a/src/cover/CoverArt.hpp b/src/cover/CoverArt.hpp deleted file mode 100644 index 4594841c..00000000 --- a/src/cover/CoverArt.hpp +++ /dev/null @@ -1,56 +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 . - */ - -#ifndef COVER_ART_HPP -#define COVER_ART_HPP - -#include -#include - - -namespace CoverArt -{ - -class CoverArt -{ - public: - typedef std::vector data_type; - - CoverArt() {} - CoverArt(const std::string& mime, const data_type& data) : _mimeType(mime), _data(data) {} - - const std::string& getMimeType() const { return _mimeType; } - const data_type& getData() const { return _data; } - - void setMimeType(const std::string& mimeType) { _mimeType = mimeType;} - void setData(const data_type& data) { _data = data; } - - bool scale(std::size_t size); - - private: - - std::string _mimeType; - data_type _data; -}; - - -} // namespace CoverArt - -#endif - diff --git a/src/cover/CoverArtGrabber.cpp b/src/cover/CoverArtGrabber.cpp index 695ddc3e..738d04f3 100644 --- a/src/cover/CoverArtGrabber.cpp +++ b/src/cover/CoverArtGrabber.cpp @@ -18,26 +18,12 @@ */ #include "logger/Logger.hpp" -#include "config/ConfigReader.hpp" -#include "av/InputFormatContext.hpp" +#include "av/AvInfo.hpp" #include "CoverArtGrabber.hpp" - namespace { -std::vector splitStrings(const std::string& source) -{ - std::vector res; - std::istringstream oss(source); - - std::string str; - while(oss >> str) - res.push_back(str); - - return res; -} - bool isFileSupported(const boost::filesystem::path& file, const std::vector extensions) { @@ -56,7 +42,6 @@ isFileSupported(const boost::filesystem::path& file, const std::vector +getFromAvMediaFile(const Av::MediaFile& input, std::size_t nbMaxCovers) { - for (const std::string& extension : splitStrings( ConfigReader::instance().getString("main.cover.file_extensions"))) - _fileExtensions.push_back("." + extension); + std::vector res; - _maxFileSize = ConfigReader::instance().getULong("main.cover.file_max_size"); -} - -std::vector -Grabber::getFromInputFormatContext(const Av::InputFormatContext& input, std::size_t nbMaxCovers) const -{ - std::vector res; - - try + for (Av::Picture& picture : input.getAttachedPictures(nbMaxCovers)) { - std::vector pictures = input.getPictures(nbMaxCovers); + Image::Image image; - for (Av::Picture& picture : pictures) - res.push_back( CoverArt(picture.mimeType, picture.data) ); - - } - catch(std::exception& e) - { - LMS_LOG(MOD_COVER, SEV_ERROR) << "Cannot get pictures: " << e.what(); + if (image.load(picture.data)) + res.push_back( image ); + else + LMS_LOG(COVER, ERROR) << "Cannot load embedded cover file in '" << input.getPath() << "'"; } return res; } -std::vector +std::vector Grabber::getFromDirectory(const boost::filesystem::path& p, std::size_t nbMaxCovers) const { - std::vector res; + std::vector res; std::vector coverPathes = getCoverPaths(p, nbMaxCovers); for (auto coverPath : coverPathes) @@ -108,14 +81,12 @@ Grabber::getFromDirectory(const boost::filesystem::path& p, std::size_t nbMaxCov if (res.size() >= nbMaxCovers) break; - std::vector data; - std::ifstream file(coverPath.string(), std::ios::binary); - char c; - while (file.get(c)) - data.push_back(c); + Image::Image image; - // TODO handle other formats - res.push_back(CoverArt("image/jpeg", data)); + if (image.load(coverPath)) + res.push_back(image); + else + LMS_LOG(COVER, ERROR) << "Cannot load image in file '" << coverPath << "'"; } return res; @@ -143,7 +114,7 @@ Grabber::getCoverPaths(const boost::filesystem::path& directoryPath, std::size_t if (boost::filesystem::file_size(path) > _maxFileSize) { - LMS_LOG(MOD_COVER, SEV_INFO) << "Cover file '" << path << " is too big (" << boost::filesystem::file_size(path) << "), limit is " << _maxFileSize; + LMS_LOG(COVER, INFO) << "Cover file '" << path << " is too big (" << boost::filesystem::file_size(path) << "), limit is " << _maxFileSize; continue; } @@ -155,27 +126,18 @@ Grabber::getCoverPaths(const boost::filesystem::path& directoryPath, std::size_t return res; } -std::vector +std::vector Grabber::getFromTrack(const boost::filesystem::path& p, std::size_t nbMaxCovers) const { - std::vector res; + Av::MediaFile input(p); - try - { - Av::InputFormatContext input(p); - - res = getFromInputFormatContext(input, nbMaxCovers); - - } - catch(std::exception& e) - { - LMS_LOG(MOD_COVER, SEV_ERROR) << "Cannot get covers from file " << p << ": " << e.what(); - } - - return res; + if (input.open()) + return getFromAvMediaFile(input, nbMaxCovers); + else + return std::vector(); } -std::vector +std::vector Grabber::getFromTrack(Wt::Dbo::Session& session, Database::Track::id_type trackId, std::size_t nbMaxCovers) const { using namespace Database; @@ -184,7 +146,7 @@ Grabber::getFromTrack(Wt::Dbo::Session& session, Database::Track::id_type trackI Track::pointer track = Track::getById(session, trackId); if (!track) - return std::vector(); + return std::vector(); Track::CoverType coverType = track->getCoverType(); boost::filesystem::path trackPath = track->getPath(); @@ -195,17 +157,15 @@ Grabber::getFromTrack(Wt::Dbo::Session& session, Database::Track::id_type trackI { case Track::CoverType::Embedded: return Grabber::getFromTrack(trackPath, nbMaxCovers); - case Track::CoverType::ExternalFile: - return Grabber::getFromDirectory(trackPath.parent_path(), nbMaxCovers); case Track::CoverType::None: - return std::vector(); + return Grabber::getFromDirectory(trackPath.parent_path(), nbMaxCovers); } - return std::vector(); + return std::vector(); } -std::vector +std::vector Grabber::getFromRelease(Wt::Dbo::Session& session, Database::Release::id_type releaseId, std::size_t nbMaxCovers) const { using namespace Database; @@ -222,14 +182,14 @@ Grabber::getFromRelease(Wt::Dbo::Session& session, Database::Release::id_type re -1, 1 /* limit result size */); if (tracks.empty()) - return std::vector(); + return std::vector(); firstTrackPath = tracks.front()->getPath(); embeddedCover = (tracks.front()->getCoverType() == Track::CoverType::Embedded); } // First, try to get covers from the directory of the release - std::vector res = getFromDirectory( firstTrackPath.parent_path(), nbMaxCovers); + std::vector res = getFromDirectory( firstTrackPath.parent_path(), nbMaxCovers); // Fallback on the embedded cover of the first track if (res.empty() && embeddedCover) diff --git a/src/cover/CoverArtGrabber.hpp b/src/cover/CoverArtGrabber.hpp index f2dbb33e..18d53a5d 100644 --- a/src/cover/CoverArtGrabber.hpp +++ b/src/cover/CoverArtGrabber.hpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 Emeric Poupon + * Copyright (C) 2015 Emeric Poupon * * This file is part of LMS. * @@ -17,16 +17,13 @@ * along with LMS. If not, see . */ -#ifndef COVER_ART_GRABBER_HPP -#define COVER_ART_GRABBER_HPP +#pragma once #include -#include "av/InputFormatContext.hpp" #include "database/Types.hpp" -#include "CoverArt.hpp" - +#include "image/Image.hpp" namespace CoverArt { @@ -38,26 +35,21 @@ class Grabber static Grabber& instance(); - void init(); - std::vector getCoverPaths(const boost::filesystem::path& directoryPath, std::size_t nbMaxCovers = 1) const; - std::vector getFromDirectory(const boost::filesystem::path& path, std::size_t nbMaxCovers = 1) const; - std::vector getFromInputFormatContext(const Av::InputFormatContext& input, std::size_t nbMaxCovers = 1) const; - std::vector getFromTrack(const boost::filesystem::path& path, std::size_t nbMaxCovers = 1) const; - std::vector getFromTrack(Wt::Dbo::Session& session, Database::Track::id_type trackId, std::size_t nbMaxCovers = 1) const; - std::vector getFromRelease(Wt::Dbo::Session& session, Database::Release::id_type releaseId, std::size_t nbMaxCovers = 1) const; + std::vector getFromDirectory(const boost::filesystem::path& path, std::size_t nbMaxCovers = 1) const; + std::vector getFromTrack(const boost::filesystem::path& path, std::size_t nbMaxCovers = 1) const; + std::vector getFromTrack(Wt::Dbo::Session& session, Database::Track::id_type trackId, std::size_t nbMaxCovers = 1) const; + std::vector getFromRelease(Wt::Dbo::Session& session, Database::Release::id_type releaseId, std::size_t nbMaxCovers = 1) const; private: Grabber(); - std::vector _fileExtensions; - std::size_t _maxFileSize; - std::vector _preferredFileNames; - - + std::vector _fileExtensions + = {".jpg", ".jpeg", ".png", ".bmp"}; // TODO parametrize + std::size_t _maxFileSize = 5000000; + std::vector _preferredFileNames + = {"cover", "front"}; // TODO parametrize }; } // namespace CoverArt - -#endif diff --git a/src/database-updater/Checksum.cpp b/src/database-updater/Checksum.cpp index f4c0dec6..ad52914b 100644 --- a/src/database-updater/Checksum.cpp +++ b/src/database-updater/Checksum.cpp @@ -47,7 +47,7 @@ void computeCrc(const boost::filesystem::path& p, std::vector& cr } else { - LMS_LOG(MOD_DBUPDATER, SEV_ERROR) << "Failed to open file '" << p << "'"; + LMS_LOG(DBUPDATER, ERROR) << "Failed to open file '" << p << "'"; throw std::runtime_error("Failed to open file '" + p.string() + "'" ); } diff --git a/src/database-updater/DatabaseUpdater.cpp b/src/database-updater/DatabaseUpdater.cpp index 2239d96c..eba1e54b 100644 --- a/src/database-updater/DatabaseUpdater.cpp +++ b/src/database-updater/DatabaseUpdater.cpp @@ -19,7 +19,6 @@ #include -#include #include #include @@ -73,7 +72,7 @@ isFileSupported(const boost::filesystem::path& file, const std::vector getRootDirectoriesByType(Wt::Dbo::Session& session, Database::MediaDirectory::Type type) { + Wt::Dbo::Transaction transaction(session); + std::vector res; std::vector rootDirs = Database::MediaDirectory::getByType(session, type); - BOOST_FOREACH(Database::MediaDirectory::pointer rootDir, rootDirs) + for (auto rootDir : rootDirs) res.push_back(rootDir->getPath()); return res; } +bool +isPathInParentPath(const boost::filesystem::path& path, const boost::filesystem::path& parentPath) +{ + boost::filesystem::path curPath = path; + + while (curPath.has_parent_path()) + { + curPath = curPath.parent_path(); + + if (curPath == parentPath) + return true; + } + + return false; +} + } // namespace @@ -102,29 +119,15 @@ namespace DatabaseUpdater { using namespace Database; -Updater::Updater(boost::filesystem::path dbPath, MetaData::Parser& parser) +Updater::Updater(Wt::Dbo::SqlConnectionPool &connectionPool, MetaData::Parser& parser) : _running(false), _scheduleTimer(_ioService), -_db(dbPath), +_db(connectionPool), _metadataParser(parser) { _ioService.setThreadCount(1); } -void -Updater::setAudioExtensions(const std::vector& extensions) -{ - BOOST_FOREACH(const std::string& extension, extensions) - _audioExtensions.push_back("." + extension); -} - -void -Updater::setVideoExtensions(const std::vector& extensions) -{ - BOOST_FOREACH(const std::string& extension, extensions) - _videoExtensions.push_back("." + extension); -} - void Updater::start(void) { @@ -155,7 +158,7 @@ Updater::processNextJob(void) MediaDirectorySettings::pointer settings = MediaDirectorySettings::get(_db.getSession()); if (settings->getManualScanRequested()) { - LMS_LOG(MOD_DBUPDATER, SEV_NOTICE) << "Manual scan requested!"; + LMS_LOG(DBUPDATER, INFO) << "Manual scan requested!"; scheduleScan( boost::posix_time::seconds(0) ); } else @@ -198,7 +201,7 @@ Updater::processNextJob(void) void Updater::scheduleScan( boost::posix_time::time_duration duration) { - LMS_LOG(MOD_DBUPDATER, SEV_NOTICE) << "Scheduling next scan in " << duration; + LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan in " << duration; _scheduleTimer.expires_from_now(duration); _scheduleTimer.async_wait( boost::bind( &Updater::process, this, boost::asio::placeholders::error) ); } @@ -206,7 +209,7 @@ Updater::scheduleScan( boost::posix_time::time_duration duration) void Updater::scheduleScan( boost::posix_time::ptime time) { - LMS_LOG(MOD_DBUPDATER, SEV_NOTICE) << "Scheduling next scan at " << time; + LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan at " << time; _scheduleTimer.expires_at(time); _scheduleTimer.async_wait( boost::bind( &Updater::process, this, boost::asio::placeholders::error) ); } @@ -216,24 +219,32 @@ Updater::process(boost::system::error_code err) { if (!err) { + updateFileExtensions(); + Stats stats; checkAudioFiles(stats); checkVideoFiles(stats); - typedef std::pair RootDirectory; std::vector rootDirectories; { Wt::Dbo::Transaction transaction(_db.getSession()); - std::vector mediaDirectories = MediaDirectory::getAll(_db.getSession()); - BOOST_FOREACH(MediaDirectory::pointer directory, mediaDirectories) - rootDirectories.push_back( std::make_pair( directory->getPath(), directory->getType() )); + + for (MediaDirectory::pointer directory : MediaDirectory::getAll(_db.getSession())) + rootDirectories.push_back( RootDirectory( directory->getType(), directory->getPath() )); } for (RootDirectory rootDirectory : rootDirectories) - processDirectory(rootDirectory.first, rootDirectory.first, rootDirectory.second, stats); + { + if (!_running) + break; - LMS_LOG(MOD_DBUPDATER, SEV_INFO) << "Changes = " << stats.nbChanges(); + LMS_LOG(DBUPDATER, INFO) << "Processing root directory '" << rootDirectory.path << "'..."; + processRootDirectory(rootDirectory, stats); + LMS_LOG(DBUPDATER, INFO) << "Processing root directory '" << rootDirectory.path << "' DONE"; + } + + LMS_LOG(DBUPDATER, INFO) << "Scan complete. Changes = " << stats.nbChanges() << ", Errors = " << stats.nbScanErrors; // Update database stats boost::posix_time::ptime now = boost::posix_time::second_clock::local_time(); @@ -261,6 +272,15 @@ Updater::process(boost::system::error_code err) } } +void +Updater::updateFileExtensions() +{ + Wt::Dbo::Transaction transaction(_db.getSession()); + + _audioFileExtensions = MediaDirectorySettings::get(_db.getSession())->getAudioFileExtensions(); + _videoFileExtensions = MediaDirectorySettings::get(_db.getSession())->getVideoFileExtensions(); +} + Artist::pointer Updater::getArtist( const boost::filesystem::path& file, const std::string& name, const std::string& mbid) { @@ -358,65 +378,59 @@ Updater::getGenres( const std::list& names) void Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats) { - try { - // Check last update time - boost::posix_time::ptime lastWriteTime (boost::posix_time::from_time_t( boost::filesystem::last_write_time( file ) ) ); + boost::posix_time::ptime lastWriteTime (boost::posix_time::from_time_t( boost::filesystem::last_write_time( file ) ) ); + // Skip file if last write is the same + { Wt::Dbo::Transaction transaction(_db.getSession()); - // Skip file if last write is the same Wt::Dbo::ptr track = Track::getByPath(_db.getSession(), file); - // if the file is the same and embeds covers, no need to update - if (track && track->getLastWriteTime() == lastWriteTime - && (track->getCoverType() == Database::Track::CoverType::Embedded)) - return; - - // Check for external covers - std::vector externalCovers = CoverArt::Grabber::instance().getCoverPaths(file.parent_path()); if (track && track->getLastWriteTime() == lastWriteTime) - { - // no change since last time we updated - // Skip only if no external covers has to be set - if (((track->getCoverType() == Database::Track::CoverType::None) && externalCovers.empty()) - || ((track->getCoverType() == Database::Track::CoverType::ExternalFile) && !externalCovers.empty())) return; - } + } - MetaData::Items items; - _metadataParser.parse(file, items); + MetaData::Items items; + if (!_metadataParser.parse(file, items)) + return; - // We estimate this is a audio file if: - // - we found a least one audio stream - // - the duration is not null - if (items.find(MetaData::Type::AudioStreams) == items.end() - || boost::any_cast >(items[MetaData::Type::AudioStreams]).empty()) + Wt::Dbo::Transaction transaction(_db.getSession()); + + Wt::Dbo::ptr track = Track::getByPath(_db.getSession(), file); + + // We estimate this is a audio file if: + // - we found a least one audio stream + // - the duration is not null + if (items.find(MetaData::Type::AudioStreams) == items.end() + || boost::any_cast >(items[MetaData::Type::AudioStreams]).empty()) + { + LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file << "' (no audio stream found)"; + + // If Track exists here, delete it! + if (track) { - LMS_LOG(MOD_DBUPDATER, SEV_DEBUG) << "Skipped '" << file << "' (no audio stream found)"; - - // If Track exists here, delete it! - if (track) { - track.remove(); - stats.nbRemoved++; - } - return; + track.remove(); + stats.nbRemoved++; } - if (items.find(MetaData::Type::Duration) == items.end() - || boost::any_cast(items[MetaData::Type::Duration]).total_seconds() <= 0) + return; + } + if (items.find(MetaData::Type::Duration) == items.end() + || boost::any_cast(items[MetaData::Type::Duration]).total_seconds() <= 0) + { + LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file << "' (no duration or duration <= 0)"; + + // If Track exists here, delete it! + if (track) { - LMS_LOG(MOD_DBUPDATER, SEV_DEBUG) << "Skipped '" << file << "' (no duration or duration <= 0)"; - - // If Track exists here, delete it! - if (track) { - track.remove(); - stats.nbRemoved++; - } - return; + track.remove(); + stats.nbRemoved++; } + return; + } - // ***** Title - std::string title; - if (items.find(MetaData::Type::Title) != items.end()) { + // ***** Title + std::string title; + if (items.find(MetaData::Type::Title) != items.end()) { title = boost::any_cast(items[MetaData::Type::Title]); } else @@ -477,12 +491,12 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats) { // Create a new song track = Track::create(_db.getSession(), file); - LMS_LOG(MOD_DBUPDATER, SEV_INFO) << "Adding '" << file << "'"; + LMS_LOG(DBUPDATER, INFO) << "Adding '" << file << "'"; stats.nbAdded++; } else { - LMS_LOG(MOD_DBUPDATER, SEV_INFO) << "Updating '" << file << "'"; + LMS_LOG(DBUPDATER, INFO) << "Updating '" << file << "'"; stats.nbModified++; } @@ -531,52 +545,39 @@ Updater::processAudioFile( const boost::filesystem::path& file, Stats& stats) { bool hasCover = boost::any_cast(items[MetaData::Type::HasCover]); - if (hasCover) - track.modify()->setCoverType( Track::CoverType::Embedded ); - else if (!externalCovers.empty()) - track.modify()->setCoverType( Track::CoverType::ExternalFile ); - else - track.modify()->setCoverType( Track::CoverType::None); + track.modify()->setCoverType( hasCover ? Track::CoverType::Embedded : Track::CoverType::None ); } transaction.commit(); - } - catch( std::exception& e ) { - LMS_LOG(MOD_DBUPDATER, SEV_ERROR) << "Exception while parsing audio file : '" << file << "': '" << e.what() << "' => skipping!"; - } } void -Updater::processDirectory(const boost::filesystem::path& rootDirectory, - const boost::filesystem::path& p, - Database::MediaDirectory::Type type, - Stats& stats) +Updater::processRootDirectory(RootDirectory rootDirectory, Stats& stats) { - if (!_running) + + if (!boost::filesystem::exists(rootDirectory.path) || !boost::filesystem::is_directory(rootDirectory.path)) return; - if (!boost::filesystem::exists(p) || !boost::filesystem::is_directory(p)) - return; - - boost::filesystem::recursive_directory_iterator itPath(rootDirectory); + boost::filesystem::recursive_directory_iterator itPath(rootDirectory.path); boost::filesystem::recursive_directory_iterator itEnd; while (itPath != itEnd) { if (!_running) return; - if (boost::filesystem::is_regular(*itPath)) { - switch( type ) + if (boost::filesystem::is_regular(*itPath)) + { + switch( rootDirectory.type ) { case Database::MediaDirectory::Audio: - if (isFileSupported(*itPath, _audioExtensions)) + if (isFileSupported(*itPath, _audioFileExtensions)) processAudioFile( *itPath, stats ); break; case Database::MediaDirectory::Video: - if (isFileSupported(*itPath, _videoExtensions)) + if (isFileSupported(*itPath, _videoFileExtensions)) processVideoFile( *itPath, stats); break; } @@ -589,119 +590,150 @@ Updater::processDirectory(const boost::filesystem::path& rootDirectory, bool Updater::checkFile(const boost::filesystem::path& p, const std::vector& rootDirs, const std::vector& extensions) { - bool status = true; + try + { + bool status = true; - // For each track, make sure the the file still exists - // and still belongs to a root directory - if (!boost::filesystem::exists( p ) - || !boost::filesystem::is_regular( p ) ) - { - LMS_LOG(MOD_DBUPDATER, SEV_INFO) << "Missing file '" << p << "'"; - status = false; - } - else - { - bool foundRoot = false; - BOOST_FOREACH(const boost::filesystem::path& rootDir, rootDirs) + // For each track, make sure the the file still exists + // and still belongs to a root directory + if (!boost::filesystem::exists( p ) + || !boost::filesystem::is_regular( p ) ) { - if (p.string().find( rootDir.string() ) != std::string::npos) + LMS_LOG(DBUPDATER, INFO) << "Missing file '" << p << "'"; + status = false; + } + else + { + + bool foundRoot = false; + for (auto& rootDir : rootDirs) { - foundRoot = true; - break; + if (isPathInParentPath(p, rootDir)) + { + foundRoot = true; + break; + } + } + + if (!foundRoot) + { + LMS_LOG(DBUPDATER, INFO) << "Out of root file '" << p << "'"; + status = false; + } + else if (!isFileSupported(p, extensions)) + { + LMS_LOG(DBUPDATER, INFO) << "File format no longer supported for '" << p << "'"; + status = false; } } - if (!foundRoot) - { - LMS_LOG(MOD_DBUPDATER, SEV_INFO) << "Out of root file '" << p << "'"; - status = false; - } - else if (!isFileSupported(p, extensions)) - { - LMS_LOG(MOD_DBUPDATER, SEV_INFO) << "File format no longer supported for '" << p << "'"; - status = false; - } + return status; + + } + catch (boost::filesystem::filesystem_error& e) + { + LMS_LOG(DBUPDATER, ERROR) << "Caught exception while checking file '" << p << "': " << e.what(); + return false; } - return status; } - void Updater::checkAudioFiles( Stats& stats ) { + LMS_LOG(DBUPDATER, INFO) << "Checking audio files..."; - LMS_LOG(MOD_DBUPDATER, SEV_INFO) << "Checking audio files..."; - Wt::Dbo::Transaction transaction(_db.getSession()); - + std::vector trackPaths = Track::getAllPaths(_db.getSession());; std::vector rootDirs = getRootDirectoriesByType(_db.getSession(), Database::MediaDirectory::Audio); - LMS_LOG(MOD_DBUPDATER, SEV_DEBUG) << "Checking tracks..."; - auto tracks = Track::getAll(_db.getSession()); - for (auto track : tracks) + LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks..."; + for (auto& trackPath : trackPaths) { - if (!checkFile(track->getPath(), rootDirs, _audioExtensions)) + if (!_running) + return; + + if (!checkFile(trackPath, rootDirs, _audioFileExtensions)) { - track.remove(); - stats.nbRemoved++; + Wt::Dbo::Transaction transaction(_db.getSession()); + + Track::pointer track = Track::getByPath(_db.getSession(), trackPath); + if (track) + { + track.remove(); + stats.nbRemoved++; + } } } - // Now process orphan Genre (no track) - LMS_LOG(MOD_DBUPDATER, SEV_DEBUG) << "Checking Genres..."; - auto genres = Genre::getAll(_db.getSession()); - for (auto genre : genres) + LMS_LOG(DBUPDATER, DEBUG) << "Checking Genres..."; { - if (genre->getTracks().size() == 0) + Wt::Dbo::Transaction transaction(_db.getSession()); + + // Now process orphan Genre (no track) + auto genres = Genre::getAll(_db.getSession()); + for (auto genre : genres) { - LMS_LOG(MOD_DBUPDATER, SEV_DEBUG) << "Removing orphan genre '" << genre->getName() << "'"; - genre.remove(); + if (genre->getTracks().size() == 0) + { + LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan genre '" << genre->getName() << "'"; + genre.remove(); + } } } - LMS_LOG(MOD_DBUPDATER, SEV_DEBUG) << "Checking artists..."; - auto artists = Artist::getAllOrphans(_db.getSession()); - for (auto artist : artists) + LMS_LOG(DBUPDATER, DEBUG) << "Checking artists..."; { - LMS_LOG(MOD_DBUPDATER, SEV_DEBUG) << "Removing orphan artist '" << artist->getName() << "'"; - artist.remove(); + Wt::Dbo::Transaction transaction(_db.getSession()); + + auto artists = Artist::getAllOrphans(_db.getSession()); + for (auto artist : artists) + { + LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan artist '" << artist->getName() << "'"; + artist.remove(); + } } - LMS_LOG(MOD_DBUPDATER, SEV_DEBUG) << "Checking releases..."; - auto releases = Release::getAllOrphans(_db.getSession()); - for (auto release : releases) + LMS_LOG(DBUPDATER, DEBUG) << "Checking releases..."; { - LMS_LOG(MOD_DBUPDATER, SEV_DEBUG) << "Removing orphan release '" << release->getName() << "'"; - release.remove(); + Wt::Dbo::Transaction transaction(_db.getSession()); + + auto releases = Release::getAllOrphans(_db.getSession()); + for (auto release : releases) + { + LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan release '" << release->getName() << "'"; + release.remove(); + } } - LMS_LOG(MOD_DBUPDATER, SEV_INFO) << "Check audio files done!"; + LMS_LOG(DBUPDATER, INFO) << "Check audio files done!"; } void Updater::checkVideoFiles( Stats& stats ) { - LMS_LOG(MOD_DBUPDATER, SEV_DEBUG) << "Checking video files..."; - Wt::Dbo::Transaction transaction(_db.getSession()); - std::vector rootDirs = getRootDirectoriesByType(_db.getSession(), Database::MediaDirectory::Video); + std::vector videoPaths = Video::getAllPaths(_db.getSession()); - LMS_LOG(MOD_DBUPDATER, SEV_DEBUG) << "Checking videos..."; - typedef Wt::Dbo::collection< Wt::Dbo::ptr