1 /*
2  * Copyright (C) 2014 Emeric Poupon
3  *
4  * This file is part of LMS.
5  *
6  * LMS is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * LMS is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with LMS.  If not, see <http://www.gnu.org/licenses/>.
18  */
19 
20 #include "DownloadResource.hpp"
21 
22 #include <array>
23 #include <iostream>
24 #include <iomanip>
25 
26 #include <Wt/Http/Response.h>
27 #include <Wt/WLocalDateTime.h>
28 
29 #include "database/Artist.hpp"
30 #include "database/Release.hpp"
31 #include "database/Session.hpp"
32 #include "database/Track.hpp"
33 #include "utils/Exception.hpp"
34 #include "utils/Logger.hpp"
35 #include "utils/Zipper.hpp"
36 
37 #include "LmsApplication.hpp"
38 
39 #define LOG(level)	LMS_LOG(UI, level) << "Download resource: "
40 
41 namespace UserInterface {
42 
~DownloadResource()43 DownloadResource::~DownloadResource()
44 {
45 	beingDeleted();
46 }
47 
48 void
handleRequest(const Wt::Http::Request & request,Wt::Http::Response & response)49 DownloadResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response)
50 {
51 	try
52 	{
53 		std::shared_ptr<Zip::Zipper> zipper;
54 
55 		// First, see if this request is for a continuation
56 		Wt::Http::ResponseContinuation *continuation = request.continuation();
57 		if (continuation)
58 			zipper = Wt::cpp17::any_cast<std::shared_ptr<Zip::Zipper>>(continuation->data());
59 		else
60 		{
61 			zipper = createZipper();
62 			response.setContentLength(zipper->getTotalZipFile());
63 			response.setMimeType("application/zip");
64 		}
65 
66 		if (!zipper)
67 			return;
68 
69 		std::array<std::byte, bufferSize> buffer;
70 		Zip::SizeType nbWrittenBytes {zipper->writeSome(buffer.data(), buffer.size())};
71 
72 		response.out().write(reinterpret_cast<const char *>(buffer.data()), nbWrittenBytes);
73 
74 		if (!zipper->isComplete())
75 		{
76 			auto* continuation {response.createContinuation()};
77 			continuation->setData(zipper);
78 		}
79 	}
80 	catch (Zip::ZipperException& exception)
81 	{
82 		LOG(ERROR) << "Zipper exception: " << exception.what();
83 	}
84 }
85 
86 
87 static
88 std::string
getArtistPathName(Database::Artist::pointer artist)89 getArtistPathName(Database::Artist::pointer artist)
90 {
91 	return StringUtils::replaceInString(artist->getName(), "/", "_");
92 }
93 
94 static
95 std::string
getReleaseArtistPathName(Database::Release::pointer release)96 getReleaseArtistPathName(Database::Release::pointer release)
97 {
98 	std::string releaseArtistName;
99 
100 	std::vector<Wt::Dbo::ptr<Database::Artist>> artists;
101 
102 	artists = release->getReleaseArtists();
103 	if (artists.empty())
104 		artists = release->getArtists();
105 
106 	if (artists.size() > 1)
107 		releaseArtistName = Wt::WString::tr("Lms.Explore.various-artists").toUTF8();
108 	else if (artists.size() == 1)
109 		releaseArtistName = artists.front()->getName();
110 
111 	releaseArtistName = StringUtils::replaceInString(releaseArtistName, "/", "_");
112 
113 	return releaseArtistName;
114 }
115 
116 static
117 std::string
getReleasePathName(Database::Release::pointer release)118 getReleasePathName(Database::Release::pointer release)
119 {
120 	std::string releaseName;
121 
122 	if (auto releaseYear {release->getReleaseYear()})
123 		releaseName += std::to_string(*releaseYear) + " - ";
124 	releaseName += StringUtils::replaceInString(release->getName(), "/", "_");
125 
126 	return releaseName;
127 }
128 
129 static
130 std::string
getTrackPathName(Database::Track::pointer track)131 getTrackPathName(Database::Track::pointer track)
132 {
133 	std::ostringstream fileName;
134 
135 	if (auto discNumber {track->getDiscNumber()})
136 		fileName << *discNumber << ".";
137 	if (auto trackNumber {track->getTrackNumber()})
138 		fileName << std::setw(2) << std::setfill('0') << *trackNumber << " - ";
139 
140 	fileName << StringUtils::replaceInString(track->getName(), "/", "_") << track->getPath().filename().extension().string();
141 
142 	return fileName.str();
143 }
144 
145 static
146 std::unique_ptr<Zip::Zipper>
createZipper(const std::vector<Database::Track::pointer> & tracks)147 createZipper(const std::vector<Database::Track::pointer>& tracks)
148 {
149 	std::map<std::string, std::filesystem::path> files;
150 
151 	for (const Database::Track::pointer& track : tracks)
152 	{
153 		std::string releaseName;
154 		std::string releaseArtistName;
155 		if (auto release {track->getRelease()})
156 		{
157 			releaseName = getReleasePathName(release);
158 			releaseArtistName = getReleaseArtistPathName(release);
159 		}
160 
161 		std::string fileName;
162 		if (!releaseArtistName.empty())
163 			fileName += releaseArtistName + "/";
164 		if (!releaseName.empty())
165 			fileName += releaseName + "/";
166 		fileName += getTrackPathName(track);
167 
168 		files.emplace(fileName, track->getPath());
169 	}
170 
171 	return std::make_unique<Zip::Zipper>(files, Wt::WLocalDateTime::currentDateTime().toUTC());
172 }
173 
DownloadArtistResource(Database::IdType artistId)174 DownloadArtistResource::DownloadArtistResource(Database::IdType artistId)
175 : _artistId {artistId}
176 {
177 	auto transaction {LmsApp->getDbSession().createSharedTransaction()};
178 
179 	Database::Artist::pointer artist {Database::Artist::getById(LmsApp->getDbSession(), artistId)};
180 	if (artist)
181 		suggestFileName(getArtistPathName(artist) + ".zip");
182 }
183 
184 std::unique_ptr<Zip::Zipper>
createZipper()185 DownloadArtistResource::createZipper()
186 {
187 	auto transaction {LmsApp->getDbSession().createSharedTransaction()};
188 
189 	const Database::Artist::pointer artist {Database::Artist::getById(LmsApp->getDbSession(), _artistId)};
190 	if (!artist)
191 	{
192 		LOG(DEBUG) << "Cannot find artist";
193 		return {};
194 	}
195 
196 	return UserInterface::createZipper(artist->getTracks());
197 }
198 
DownloadReleaseResource(Database::IdType releaseId)199 DownloadReleaseResource::DownloadReleaseResource(Database::IdType releaseId)
200 : _releaseId {releaseId}
201 {
202 	auto transaction {LmsApp->getDbSession().createSharedTransaction()};
203 
204 	Database::Release::pointer release {Database::Release::getById(LmsApp->getDbSession(), releaseId)};
205 	if (release)
206 		suggestFileName(getReleasePathName(release) + ".zip");
207 }
208 
209 
210 std::unique_ptr<Zip::Zipper>
createZipper()211 DownloadReleaseResource::createZipper()
212 {
213 	auto transaction {LmsApp->getDbSession().createSharedTransaction()};
214 
215 	const Database::Release::pointer release {Database::Release::getById(LmsApp->getDbSession(), _releaseId)};
216 	if (!release)
217 	{
218 		LOG(DEBUG) << "Cannot find release";
219 		return {};
220 	}
221 
222 	return UserInterface::createZipper(release->getTracks());
223 }
224 
DownloadTrackResource(Database::IdType trackId)225 DownloadTrackResource::DownloadTrackResource(Database::IdType trackId)
226 : _trackId {trackId}
227 {
228 	auto transaction {LmsApp->getDbSession().createSharedTransaction()};
229 
230 	Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), trackId)};
231 	if (track)
232 		suggestFileName(getTrackPathName(track) + ".zip");
233 }
234 
235 std::unique_ptr<Zip::Zipper>
createZipper()236 DownloadTrackResource::createZipper()
237 {
238 	auto transaction {LmsApp->getDbSession().createSharedTransaction()};
239 
240 	const Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), _trackId)};
241 	if (!track)
242 	{
243 		LOG(DEBUG) << "Cannot find track";
244 		return {};
245 	}
246 
247 	return UserInterface::createZipper({track});
248 }
249 
250 } // namespace UserInterface
251