diff --git a/approot/messages.xml b/approot/messages.xml index 146afb5e..d60cd82c 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -115,6 +115,7 @@ Add filter All Artists +Download Filter added Filters Links diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 11dc324e..310fe585 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -115,6 +115,7 @@ Ajouter filtre Tous Artistes +Télécharger Filtre ajouté Filtres Liens diff --git a/src/libs/utils/CMakeLists.txt b/src/libs/utils/CMakeLists.txt index 89505632..37a21f76 100644 --- a/src/libs/utils/CMakeLists.txt +++ b/src/libs/utils/CMakeLists.txt @@ -10,6 +10,7 @@ add_library(lmsutils SHARED impl/String.cpp impl/UUID.cpp impl/WtLogger.cpp + impl/Zipper.cpp ) target_include_directories(lmsutils INTERFACE diff --git a/src/libs/utils/impl/Path.cpp b/src/libs/utils/impl/Path.cpp index 0bfc807b..886ad9aa 100644 --- a/src/libs/utils/impl/Path.cpp +++ b/src/libs/utils/impl/Path.cpp @@ -32,14 +32,13 @@ #include "utils/Exception.hpp" #include "utils/Logger.hpp" -void -computeCrc(const std::filesystem::path& p, std::vector& crc) +std::uint32_t +computeCrc32(const std::filesystem::path& p) { using crc_type = boost::crc_32_type; crc_type result; - std::ifstream ifs( p.string().c_str(), std::ios_base::binary ); - + std::ifstream ifs {p.string().c_str(), std::ios_base::binary}; if (ifs) { do @@ -49,7 +48,7 @@ computeCrc(const std::filesystem::path& p, std::vector& crc) ifs.read( buffer.data(), buffer.size() ); result.process_bytes( buffer.data(), ifs.gcount() ); } - while ( ifs ); + while (ifs); } else { @@ -58,13 +57,7 @@ computeCrc(const std::filesystem::path& p, std::vector& crc) } - // Copy the result into a vector of unsigned char - const crc_type::value_type checksum = result.checksum(); - for (std::size_t i = 0; (i+1)*8 <= crc_type::bit_count; i++) - { - const unsigned char* data = reinterpret_cast( &checksum ); - crc.push_back(data[i]); - } + return result.checksum(); } bool diff --git a/src/libs/utils/impl/Zipper.cpp b/src/libs/utils/impl/Zipper.cpp new file mode 100644 index 00000000..f81fa33d --- /dev/null +++ b/src/libs/utils/impl/Zipper.cpp @@ -0,0 +1,499 @@ +/* + * Copyright (C) 2020 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 "utils/Zipper.hpp" + +#include +#include + +#include "utils/Path.hpp" +#include "utils/Logger.hpp" + +// Done using specs from https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT + +namespace Zip +{ + + class ZipHeader + { + public: + ZipHeader(std::byte* buffer, std::size_t bufferSize) + : _buffer {buffer} + , _bufferSize {bufferSize} + {} + + enum GeneralPurposeFlag : std::uint16_t + { + UseDataDescriptor = 1 << 3, + }; + + enum CompressionMethod : std::uint16_t + { + NoCompression = 0, + }; + + + + protected: + void write8(std::size_t offset, std::uint8_t value); + void write16(std::size_t offset, std::uint16_t value); + void write32(std::size_t offset, std::uint32_t value); + + private: + std::byte* _buffer {}; + std::size_t _bufferSize {}; + }; + + void + ZipHeader::write8(std::size_t offset, std::uint8_t value) + { + _buffer[offset] = static_cast(value); + } + + void + ZipHeader::write16(std::size_t offset, std::uint16_t value) + { + _buffer[offset] = static_cast(value & 0xff); + _buffer[offset + 1] = static_cast(value >> 8); + } + + void + ZipHeader::write32(std::size_t offset, std::uint32_t value) + { + _buffer[offset] = static_cast(value & 0xff); + _buffer[offset + 1] = static_cast((value >> 8) & 0xff); + _buffer[offset + 2] = static_cast((value >> 16) & 0xff); + _buffer[offset + 3] = static_cast(value >> 24); + } + + class LocalFileHeader : public ZipHeader + { + public: + using ZipHeader::ZipHeader; + + // Setters + void setSignature(); + void setVersionNeededToExtract(unsigned major, unsigned minor); + void setGeneralPurposeFlags(std::uint16_t flags); + void setCompressionMethod(CompressionMethod compressionMethod); + void setLastModifiedDateTime(); + void setCrc32UncompressedData(std::uint32_t crc); + void setCompressedSize(std::size_t size); + void setUncompressedSize(std::size_t size); + void setFileNameLength(std::size_t size); + void setExtraFieldLength(std::size_t size); + static constexpr std::size_t getHeaderSize() { return 30; } + }; + + void + LocalFileHeader::setSignature() + { + write32(0, 0x04034b50); + } + + void + LocalFileHeader::setVersionNeededToExtract(unsigned major, unsigned minor) + { + assert(minor < 10); + write16(4, major*10 + minor); + } + + void + LocalFileHeader::setGeneralPurposeFlags(std::uint16_t flags) + { + write16(6, flags); + } + + void + LocalFileHeader::setCompressionMethod(CompressionMethod compressionMethod) + { + write16(8, compressionMethod); + } + + void + LocalFileHeader::setLastModifiedDateTime() + { + // TODO + write16(10, 0); // time + write16(12, 0); // date + } + + void + LocalFileHeader::setCrc32UncompressedData(std::uint32_t crc) + { + write32(14, crc); + } + + void + LocalFileHeader::setCompressedSize(std::size_t size) + { + write32(18, size); + } + + void + LocalFileHeader::setUncompressedSize(std::size_t size) + { + write32(22, size); + } + + void + LocalFileHeader::setFileNameLength(std::size_t size) + { + write16(26, size); + } + + void + LocalFileHeader::setExtraFieldLength(std::size_t size) + { + write16(28, size); + } + + + class CentralDirectoryHeader : public ZipHeader + { + public: + using ZipHeader::ZipHeader; + + void setSignature() { write32(0, 0x02014b50); } + void setVersionMadeBy(unsigned major, unsigned minor) { assert(minor < 10); write16(4, major * 10 + minor); } + void setVersionNeededToExtract(unsigned major, unsigned minor) { assert(minor < 10); write16(6, major*10 + minor); } + void setGeneralPurposeFlags(std::uint16_t flags) { write16(8, flags); } + void setCompressionMethod(CompressionMethod method) { write16(10, method); } + void setLastModifiedDateTime() + { + write16(12, 0); // time + write16(14, 0); // date + } + void setCrc32UncompressedData(std::uint32_t crc32) { write32(16, crc32); } + void setCompressedSize(std::size_t size) { write32(20, size); } + void setUncompressedSize(std::size_t size) { write32(24, size); } + void setFileNameLength(std::size_t size) { write16(28, size); } + void setExtraFieldLength(std::size_t size) { write16(30, size); } + void setFileCommentLength(std::size_t size) { write16(32, size); } + void setDiskNumber(std::size_t number) { write16(34, number); } + void setInternalFileAttributes(std::uint16_t attributes) { write16(36, attributes); } + void setExternalFileAttributes(std::uint16_t attributes) { write32(38, attributes); } + void setRelativeFileHeaderOffset(std::size_t offset) { write32(42, offset); } + static constexpr std::size_t getHeaderSize() { return 46; } + }; + + class EndOfCentralDirectoryRecord : public ZipHeader + { + public: + using ZipHeader::ZipHeader; + + void setSignature() { write32(0, 0x06054b50); } + void setDiskNumber(unsigned number) { write16(4, number); } + void setCentralDirectoryDiskNumber(unsigned number) { write16(6, number); } + void setNbDiskCentralDirectoryRecords(unsigned number) { write16(8, number); } + void setNbCentralDirectoryRecords(unsigned number) { write16(10, number); } + void setCentralDirectorySize(std::size_t size) { write32(12, size); } + void setCentralDirectoryOffset(std::size_t offset) { write32(16, offset); } + void setCommentLength(std::size_t length) { write16(20, length); } + static constexpr std::size_t getHeaderSize() { return 22; } + }; + + Zipper::Zipper(const std::map& files, CompressionMethod compMethod) + : _compMethod {compMethod} + { + for (const auto& [filename, filePath] : files) + { + FileContext fileContext; + fileContext.filePath = filePath; + + std::error_code ec; + fileContext.fileSize = std::filesystem::file_size(filePath, ec); + if (ec) + { + LMS_LOG(UTILS, INFO) << "Cannot get file size for '" << filePath.string() << "': " << ec.message(); + continue; + } + fileContext.fileCrc32 = computeCrc32(filePath); + + LMS_LOG(UTILS, DEBUG) << "Processing '" << filePath.string() << "': File size = " << fileContext.fileSize; + + _files[filename] = std::move(fileContext); + } + + _currentFile = std::begin(_files); + } + + std::size_t + Zipper::writeSome(std::byte* buffer, std::size_t bufferSize) + { + // make sure we have some room for the headers + assert(bufferSize >= minOutputBufferSize); + + std::size_t nbTotalWrittenBytes {}; + + while (!isComplete() && (bufferSize >= minOutputBufferSize)) + { + std::size_t nbWrittenBytes {}; + + LMS_LOG(UTILS, DEBUG) << "Global offset = " << _currentZipOffset; + LMS_LOG(UTILS, DEBUG) << "Buffer ptr = " << buffer << ", remaining size = " << bufferSize; + + switch (_writeState) + { + case WriteState::LocalFileHeader: + nbWrittenBytes = writeLocalFileHeader(buffer, bufferSize); + break; + + case WriteState::LocalFileHeaderFileName: + nbWrittenBytes = writeLocalFileHeaderFileName(buffer, bufferSize); + break; + + case WriteState::FileData: + nbWrittenBytes = writeFileData(buffer, bufferSize); + break; + + case WriteState::CentralDirectoryHeader: + nbWrittenBytes = writeCentralDirectoryHeader(buffer, bufferSize); + break; + + case WriteState::CentralDirectoryHeaderFileName: + nbWrittenBytes = writeCentralDirectoryHeaderFileName(buffer, bufferSize); + break; + + case WriteState::EndOfCentralDirectoryRecord: + nbWrittenBytes = writeEndOfCentralDirectoryRecord(buffer, bufferSize); + break; + + case WriteState::Complete: + break; + } + + LMS_LOG(UI, DEBUG) << "nbWrittenBytes = " << nbWrittenBytes; + + buffer += nbWrittenBytes; + bufferSize -= nbWrittenBytes; + _currentZipOffset += nbWrittenBytes; + nbTotalWrittenBytes += nbWrittenBytes ; + } + + return nbTotalWrittenBytes; + } + + bool + Zipper::isComplete() const + { + return _writeState == WriteState::Complete; + } + + std::size_t + Zipper::writeLocalFileHeader(std::byte* buffer, std::size_t bufferSize) + { + static_assert(LocalFileHeader::getHeaderSize() <= minOutputBufferSize); + + assert(bufferSize >= minOutputBufferSize); + + if (_currentFile == std::cend(_files)) + { + _currentFile = std::begin(_files); + _writeState = WriteState::CentralDirectoryHeader; + return 0; + } + + LMS_LOG(UTILS, INFO) << "writeLocalFileHeader. crc = " << _currentFile->second.fileCrc32; + LocalFileHeader header {buffer, bufferSize}; + + header.setSignature(); + header.setVersionNeededToExtract(1, 0); + header.setGeneralPurposeFlags(0); + header.setCrc32UncompressedData(_currentFile->second.fileCrc32); + switch (_compMethod) + { + case CompressionMethod::NoCompression: + header.setCompressionMethod(ZipHeader::CompressionMethod::NoCompression); + header.setCompressedSize(_currentFile->second.fileSize); + header.setUncompressedSize(_currentFile->second.fileSize); + break; + } + header.setLastModifiedDateTime(); // getLastWriteTime(*_currentFile)); + header.setFileNameLength(_currentFile->first.size()); + header.setExtraFieldLength(0); + + _writeState = WriteState::LocalFileHeaderFileName; + _currentFile->second.localFileHeaderOffset = _currentZipOffset; + + return header.getHeaderSize(); + } + + std::size_t + Zipper::writeLocalFileHeaderFileName(std::byte* buffer, std::size_t bufferSize) + { + const std::string& fileName {_currentFile->first}; + + assert(_currentOffset <= fileName.size()); + if (_currentOffset == fileName.size()) + { + _writeState = WriteState::FileData; + _currentOffset = 0; + return 0; + } + + LMS_LOG(UTILS, INFO) << "writeLocalFileHeaderFileName"; + + const std::size_t nbBytesToCopy {std::min(fileName.size() - _currentOffset, bufferSize)}; + LMS_LOG(UTILS, INFO) << "\tnbBytesToCopy = " << nbBytesToCopy; + + std::copy(std::next(std::begin(fileName), _currentOffset), std::next(std::begin(fileName), nbBytesToCopy), reinterpret_cast(buffer)); + + _currentOffset += nbBytesToCopy; + return nbBytesToCopy; + } + + std::size_t + Zipper::writeFileData(std::byte* buffer, std::size_t bufferSize) + { + if (_currentOffset == _currentFile->second.fileSize) + { + _currentOffset = 0; + ++_currentFile; + _writeState = WriteState::LocalFileHeader; + + return 0; + } + + const std::string filePath {_currentFile->second.filePath.string()}; + + std::ifstream ifs {filePath.c_str(), std::ios_base::binary}; + if (!ifs) + throw ZipperException {"File '" + filePath + "' does no longer exist!"}; + + ifs.seekg(0, std::ios::end); + const ::uint64_t fileSize {static_cast<::uint64_t>(ifs.tellg())}; + ifs.seekg(0, std::ios::beg); + + if (fileSize != _currentFile->second.fileSize) + throw ZipperException {"File '" + filePath + "': size mismatch!"}; + + const std::size_t nbBytesToRead {std::min(fileSize - _currentOffset, bufferSize)}; + + ifs.seekg(_currentOffset, std::ios::beg); + ifs.read(reinterpret_cast(buffer), nbBytesToRead ); + const ::uint64_t actualReadSize {static_cast<::uint64_t>(ifs.gcount())}; + + LMS_LOG(UTILS, INFO) << "writeFileData: to read = " << nbBytesToRead << ", actually read = " << actualReadSize; + + _currentOffset += actualReadSize; + + return actualReadSize; + } + + std::size_t + Zipper::writeCentralDirectoryHeader(std::byte* buffer, std::size_t bufferSize) + { + assert(bufferSize >= minOutputBufferSize); + static_assert(CentralDirectoryHeader::getHeaderSize() <= minOutputBufferSize); + + if (_currentFile == std::begin(_files)) + { + LMS_LOG(UI, INFO) << "First record! _currentZipOffset = " << _currentZipOffset; + _centralDirectoryOffset = _currentZipOffset; + } + + if (_currentFile == std::end(_files)) + { + _writeState = WriteState::EndOfCentralDirectoryRecord; + _currentFile = std::begin(_files); + return 0; + } + + LMS_LOG(UTILS, INFO) << "writeCentralDirectoryHeader. Relative offset = " << _currentFile->second.localFileHeaderOffset << ", crc = " << std::hex << _currentFile->second.fileCrc32; + + CentralDirectoryHeader header {buffer, bufferSize}; + header.setSignature(); + header.setVersionMadeBy(2, 0); + header.setVersionNeededToExtract(1, 0); + header.setGeneralPurposeFlags(0); + switch (_compMethod) + { + case CompressionMethod::NoCompression: + header.setCompressionMethod(ZipHeader::CompressionMethod::NoCompression); + header.setCompressedSize(_currentFile->second.fileSize); + header.setUncompressedSize(_currentFile->second.fileSize); + break; + } + header.setLastModifiedDateTime(); // getLastWriteTime(*_currentFile)); + header.setCrc32UncompressedData(_currentFile->second.fileCrc32); + header.setFileNameLength(_currentFile->first.size()); + header.setExtraFieldLength(0); + header.setFileCommentLength(0); + header.setDiskNumber(0); + header.setInternalFileAttributes(0); + header.setExternalFileAttributes(0); + header.setRelativeFileHeaderOffset(_currentFile->second.localFileHeaderOffset); + + _writeState = WriteState::CentralDirectoryHeaderFileName; + _centralDirectorySize += header.getHeaderSize(); + + return header.getHeaderSize(); + } + + std::size_t + Zipper::writeCentralDirectoryHeaderFileName(std::byte* buffer, std::size_t bufferSize) + { + const std::string& fileName {_currentFile->first}; + + assert(_currentOffset <= fileName.size()); + if (_currentOffset == fileName.size()) + { + _currentOffset = 0; + ++_currentFile; + _writeState = WriteState::CentralDirectoryHeader; + + return 0; + } + + LMS_LOG(UTILS, INFO) << "writeCentralDirectoryHeaderFileName"; + const std::size_t nbBytesToCopy {std::min(fileName.size() - _currentOffset, bufferSize)}; + + std::copy(std::next(std::begin(fileName), _currentOffset), std::next(std::begin(fileName), nbBytesToCopy), reinterpret_cast(buffer)); + + _currentOffset += nbBytesToCopy; + _centralDirectorySize += nbBytesToCopy; + return nbBytesToCopy; + } + + + std::size_t + Zipper::writeEndOfCentralDirectoryRecord(std::byte* buffer, std::size_t bufferSize) + { + assert(bufferSize >= minOutputBufferSize); + static_assert(EndOfCentralDirectoryRecord::getHeaderSize() <= minOutputBufferSize); + + EndOfCentralDirectoryRecord record {buffer, bufferSize}; + + LMS_LOG(UTILS, DEBUG) << "Writing EOR. nb records = " << _files.size() << ", offset = " << _centralDirectoryOffset << ", size = " << _centralDirectorySize; + + record.setSignature(); + record.setDiskNumber(0); + record.setCentralDirectoryDiskNumber(0); + record.setNbDiskCentralDirectoryRecords(_files.size()); + record.setNbCentralDirectoryRecords(_files.size()); + record.setCentralDirectorySize(_centralDirectorySize); + record.setCentralDirectoryOffset(_centralDirectoryOffset); + record.setCommentLength(0); + + _writeState = WriteState::Complete; + return record.getHeaderSize(); + } + +} // namespace Zip diff --git a/src/libs/utils/include/utils/Path.hpp b/src/libs/utils/include/utils/Path.hpp index 573375e5..659442f9 100644 --- a/src/libs/utils/include/utils/Path.hpp +++ b/src/libs/utils/include/utils/Path.hpp @@ -26,7 +26,7 @@ #include -void computeCrc(const std::filesystem::path& p, std::vector& checksum); +std::uint32_t computeCrc32(const std::filesystem::path& p); // Make sure the given path is a directory // Create it if needed diff --git a/src/libs/utils/include/utils/String.hpp b/src/libs/utils/include/utils/String.hpp index d4655931..82f05f29 100644 --- a/src/libs/utils/include/utils/String.hpp +++ b/src/libs/utils/include/utils/String.hpp @@ -71,6 +71,7 @@ template<> std::optional readAs(const std::string& str); +[[nodiscard]] std::string replaceInString(const std::string& str, const std::string& from, const std::string& to); diff --git a/src/libs/utils/include/utils/Zipper.hpp b/src/libs/utils/include/utils/Zipper.hpp new file mode 100644 index 00000000..4692f5c8 --- /dev/null +++ b/src/libs/utils/include/utils/Zipper.hpp @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2020 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 + +#include "Exception.hpp" + +namespace Zip +{ + + class ZipperException : public LmsException + { + using LmsException::LmsException; + }; + + // Very simple on-the-fly zip creator + class Zipper + { + public: + + enum class CompressionMethod + { + NoCompression, + }; + + Zipper(const std::map& files, CompressionMethod comp = CompressionMethod::NoCompression); + + static constexpr std::size_t minOutputBufferSize = 64; + std::size_t writeSome(std::byte* buffer, std::size_t bufferSize); + bool isComplete() const; + + private: + void setComplete(); + + std::size_t writeLocalFileHeader(std::byte* buffer, std::size_t bufferSize); + std::size_t writeLocalFileHeaderFileName(std::byte* buffer, std::size_t bufferSize); + std::size_t writeFileData(std::byte* buffer, std::size_t bufferSize); + std::size_t writeCentralDirectoryHeader(std::byte* buffer, std::size_t bufferSize); + std::size_t writeCentralDirectoryHeaderFileName(std::byte* buffer, std::size_t bufferSize); + std::size_t writeEndOfCentralDirectoryRecord(std::byte* buffer, std::size_t bufferSize); + + struct FileContext + { + std::filesystem::path filePath; + std::size_t fileSize; + std::uint32_t fileCrc32; + std::size_t localFileHeaderOffset {}; + }; + + using FileContainer = std::map; + FileContainer _files; + + enum class WriteState + { + LocalFileHeader, + LocalFileHeaderFileName, + FileData, + CentralDirectoryHeader, + CentralDirectoryHeaderFileName, + EndOfCentralDirectoryRecord, + Complete, + }; + + CompressionMethod _compMethod {CompressionMethod::NoCompression}; + WriteState _writeState {WriteState::LocalFileHeader}; + FileContainer::iterator _currentFile; + std::size_t _currentOffset {}; + std::size_t _currentZipOffset {}; + std::size_t _centralDirectoryOffset {}; + std::size_t _centralDirectorySize {}; + }; + +} // namespace Zip + diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index 88905053..6bb7e998 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -31,6 +31,7 @@ add_executable(lms ui/explore/TracksView.cpp ui/resource/AudioFileResource.cpp ui/resource/AudioTranscodeResource.cpp + ui/resource/DownloadResource.cpp ui/resource/ImageResource.cpp ) diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index adeb8ce7..35dcc8c9 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -45,6 +45,7 @@ #include "admin/UsersView.hpp" #include "resource/AudioFileResource.hpp" #include "resource/AudioTranscodeResource.hpp" +#include "resource/DownloadResource.hpp" #include "resource/ImageResource.hpp" #include "Auth.hpp" #include "LmsApplicationException.hpp" @@ -429,9 +430,9 @@ LmsApplication::handleUserLoggedIn(Database::IdType userId, bool strongAuth) void LmsApplication::createHome() { - _imageResource = std::make_shared(); - _audioTranscodeResource = std::make_shared(); _audioFileResource = std::make_shared(); + _audioTranscodeResource = std::make_shared(); + _imageResource = std::make_shared(); declareJavaScriptFunction("onLoadCover", "function(id) { id.className += \" Lms-cover-loaded\"}"); doJavaScript("$('body').tooltip({ selector: '[data-toggle=\"tooltip\"]'})"); diff --git a/src/lms/ui/explore/ArtistView.cpp b/src/lms/ui/explore/ArtistView.cpp index 4f86b4a0..71f58b6a 100644 --- a/src/lms/ui/explore/ArtistView.cpp +++ b/src/lms/ui/explore/ArtistView.cpp @@ -31,7 +31,7 @@ #include "utils/Logger.hpp" #include "utils/String.hpp" -#include "resource/ImageResource.hpp" +#include "resource/DownloadResource.hpp" #include "ArtistListHelpers.hpp" #include "Filters.hpp" #include "LmsApplication.hpp" @@ -131,6 +131,8 @@ Artist::refreshView() { artistsAction.emit(PlayQueueAction::PlayLast, {*artistId}); }); + popup->addItem(Wt::WString::tr("Lms.Explore.download")) + ->setLink(Wt::WLink {std::make_unique(*artistId)}); popup->exec(moreBtn); }); diff --git a/src/lms/ui/explore/ReleasePopup.cpp b/src/lms/ui/explore/ReleasePopup.cpp index 7ce4d099..19755b33 100644 --- a/src/lms/ui/explore/ReleasePopup.cpp +++ b/src/lms/ui/explore/ReleasePopup.cpp @@ -19,6 +19,7 @@ #include "ReleasePopup.hpp" +#include "resource/DownloadResource.hpp" #include "LmsApplication.hpp" namespace UserInterface @@ -41,6 +42,8 @@ namespace UserInterface { releasesAction.emit(PlayQueueAction::PlayLast, {releaseId}); }); + popup->addItem(Wt::WString::tr("Lms.Explore.download")) + ->setLink(Wt::WLink {std::make_unique(releaseId)}); popup->popup(&target); } diff --git a/src/lms/ui/resource/DownloadResource.cpp b/src/lms/ui/resource/DownloadResource.cpp new file mode 100644 index 00000000..1466a45e --- /dev/null +++ b/src/lms/ui/resource/DownloadResource.cpp @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2014 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "DownloadResource.hpp" + +#include +#include + +#include "database/Artist.hpp" +#include "database/Release.hpp" +#include "database/Track.hpp" +#include "utils/Exception.hpp" +#include "utils/Logger.hpp" +#include "utils/Zipper.hpp" + +#include "LmsApplication.hpp" + +#define LOG(level) LMS_LOG(UI, level) << "Download resource: " + +namespace UserInterface { + +DownloadResource::~DownloadResource() +{ + beingDeleted(); +} + +void +DownloadResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) +{ + try + { + std::shared_ptr zipper; + + // First, see if this request is for a continuation + Wt::Http::ResponseContinuation *continuation = request.continuation(); + if (continuation) + zipper = Wt::cpp17::any_cast>(continuation->data()); + else + { + zipper = createZipper(); + response.setMimeType("application/zip"); + } + + if (!zipper) + return; + + std::array buffer; + std::size_t nbWrittenBytes {zipper->writeSome(buffer.data(), buffer.size())}; + + response.out().write(reinterpret_cast(buffer.data()), nbWrittenBytes); + + if (!zipper->isComplete()) + { + auto* continuation {response.createContinuation()}; + continuation->setData(zipper); + } + } + catch (Zip::ZipperException& exception) + { + LOG(ERROR) << "Zipper exception: " << exception.what(); + } +} + + +static +std::string +getArtistPathName(Database::Artist::pointer artist) +{ + return StringUtils::replaceInString(artist->getName(), "/", "_"); +} + +static +std::string +getReleaseArtistPathName(Database::Release::pointer release) +{ + std::string releaseArtistName; + + std::vector> artists; + + artists = release->getReleaseArtists(); + if (artists.empty()) + artists = release->getArtists(); + + if (artists.size() > 1) + releaseArtistName = Wt::WString::tr("Lms.Explore.various-artists").toUTF8(); + else if (artists.size() == 1) + releaseArtistName = artists.front()->getName(); + + releaseArtistName = StringUtils::replaceInString(releaseArtistName, "/", "_"); + + return releaseArtistName; +} + +static +std::string +getReleasePathName(Database::Release::pointer release) +{ + std::string releaseName; + + if (auto releaseYear {release->getReleaseYear()}) + releaseName += std::to_string(*releaseYear) + " - "; + releaseName += StringUtils::replaceInString(release->getName(), "/", "_"); + + return releaseName; +} + +static +std::string +getTrackPathName(Database::Track::pointer track) +{ + std::string fileName; + + auto trackNumber {track->getTrackNumber()}; + auto discNumber {track->getDiscNumber()}; + + if (discNumber) + fileName += std::to_string(*discNumber) + "."; + if (trackNumber) + fileName += std::to_string(*trackNumber) + " - "; + + fileName += track->getName() + track->getPath().filename().extension().string(); + fileName = StringUtils::replaceInString(fileName , "/", "_"); + + return fileName; +} + +static +std::unique_ptr +createZipper(const std::vector& tracks) +{ + std::map files; + + for (const Database::Track::pointer& track : tracks) + { + std::string releaseName; + std::string releaseArtistName; + if (auto release {track->getRelease()}) + { + releaseName = getReleasePathName(release); + releaseArtistName = getReleaseArtistPathName(release); + } + + std::string fileName; + if (!releaseArtistName.empty()) + fileName += releaseArtistName + "/"; + if (!releaseName.empty()) + fileName += releaseName + "/"; + fileName += getTrackPathName(track); + + files.emplace(fileName, track->getPath()); + } + + return std::make_unique(files, Zip::Zipper::CompressionMethod::NoCompression); +} + +DownloadReleaseResource::DownloadReleaseResource(Database::IdType releaseId) +: _releaseId {releaseId} +{ + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + + Database::Release::pointer release {Database::Release::getById(LmsApp->getDbSession(), releaseId)}; + if (release) + suggestFileName(getReleasePathName(release) + ".zip"); +} + + +std::unique_ptr +DownloadReleaseResource::createZipper() +{ + Wt::WApplication::UpdateLock lock {LmsApp}; // DbSession are not thread safe + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + + const Database::Release::pointer release {Database::Release::getById(LmsApp->getDbSession(), _releaseId)}; + if (!release) + { + LOG(DEBUG) << "Cannot find release"; + return {}; + } + + return UserInterface::createZipper(release->getTracks()); +} + +DownloadArtistResource::DownloadArtistResource(Database::IdType artistId) +: _artistId {artistId} +{ + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + + Database::Artist::pointer artist {Database::Artist::getById(LmsApp->getDbSession(), artistId)}; + if (artist) + suggestFileName(getArtistPathName(artist) + ".zip"); +} + +std::unique_ptr +DownloadArtistResource::createZipper() +{ + Wt::WApplication::UpdateLock lock {LmsApp}; // DbSession are not thread safe + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + + const Database::Artist::pointer artist {Database::Artist::getById(LmsApp->getDbSession(), _artistId)}; + if (!artist) + { + LOG(DEBUG) << "Cannot find artist"; + return {}; + } + + return UserInterface::createZipper(artist->getTracks()); +} + +} // namespace UserInterface diff --git a/src/lms/ui/resource/DownloadResource.hpp b/src/lms/ui/resource/DownloadResource.hpp new file mode 100644 index 00000000..8b29ff8a --- /dev/null +++ b/src/lms/ui/resource/DownloadResource.hpp @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2020 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 "database/Types.hpp" +#include "utils/Zipper.hpp" + +namespace UserInterface { + +class DownloadResource : public Wt::WResource +{ + public: + static constexpr std::size_t bufferSize {32768}; + + ~DownloadResource(); + + private: + + void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; + virtual std::unique_ptr createZipper() = 0; +}; + +class DownloadReleaseResource : public DownloadResource +{ + public: + DownloadReleaseResource(Database::IdType releaseId); + + private: + std::unique_ptr createZipper() override; + Database::IdType _releaseId; +}; + +class DownloadArtistResource : public DownloadResource +{ + public: + DownloadArtistResource(Database::IdType artistId); + + private: + std::unique_ptr createZipper() override; + Database::IdType _artistId; +}; + +} // namespace UserInterface + diff --git a/src/lms/ui/resource/ImageResource.hpp b/src/lms/ui/resource/ImageResource.hpp index 00865805..dc8756d2 100644 --- a/src/lms/ui/resource/ImageResource.hpp +++ b/src/lms/ui/resource/ImageResource.hpp @@ -47,7 +47,8 @@ class ImageResource : public Wt::WResource static std::string getMimeType(); - void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response); + private: + void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; }; diff --git a/src/tools/CMakeLists.txt b/src/tools/CMakeLists.txt index e5877a5b..5c970f2a 100644 --- a/src/tools/CMakeLists.txt +++ b/src/tools/CMakeLists.txt @@ -1,5 +1,6 @@ add_subdirectory(metadata) add_subdirectory(recommendation) +add_subdirectory(zipper) diff --git a/src/tools/zipper/CMakeLists.txt b/src/tools/zipper/CMakeLists.txt new file mode 100644 index 00000000..8a588796 --- /dev/null +++ b/src/tools/zipper/CMakeLists.txt @@ -0,0 +1,9 @@ + +add_executable(lms-zipper + LmsZipper.cpp + ) + +target_link_libraries(lms-zipper PRIVATE + lmsutils + ) + diff --git a/src/tools/zipper/LmsZipper.cpp b/src/tools/zipper/LmsZipper.cpp new file mode 100644 index 00000000..8cdc92a4 --- /dev/null +++ b/src/tools/zipper/LmsZipper.cpp @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2020 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 "utils/Service.hpp" +#include "utils/StreamLogger.hpp" +#include "utils/Zipper.hpp" + +int main(int argc, char* argv[]) +{ + // log to stdout + Service logger {std::make_unique(std::cout)}; + + if (argc < 2) + { + std::cerr << "Usage: [...]" << std::endl; + return EXIT_FAILURE; + } + + std::filesystem::path zipPath {argv[1]}; + + std::map files; + for (int i {2}; i < argc; ++i) + files.emplace(argv[i], argv[i]); + + + std::cout << "Compressing " << files.size() << " files..." << std::endl; + + using namespace Zip; + + std::ofstream ofs {zipPath.string().c_str(), std::ios_base::binary}; + if (!ofs) + { + std::cerr << "Cannot open file '" << zipPath.string() << "' for writing"; + return EXIT_FAILURE; + } + + Zipper zipper {files, Zipper::CompressionMethod::NoCompression}; + + while (!zipper.isComplete()) + { + std::array buffer; + + std::cout << "Call" << std::endl; + std::size_t nbWrittenBytes {zipper.writeSome(buffer.data(), buffer.size())}; + std::cout << "nbWrittenBytes = " << nbWrittenBytes << std::endl; + + ofs.write(reinterpret_cast(buffer.data()), nbWrittenBytes); + } + + return EXIT_SUCCESS; +}