mirror of
https://github.com/steredbits/pelagia.git
synced 2026-10-17 19:01:27 +02:00
203 lines
7.9 KiB
C++
203 lines
7.9 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Copyright (C) 2026 steredbits and Pelagia contributors
|
|
#include "api/jellyfin_urls.h"
|
|
|
|
#include <cstdio>
|
|
|
|
namespace api {
|
|
|
|
std::string url_encode(const std::string& value) {
|
|
std::string out;
|
|
out.reserve(value.size());
|
|
for (unsigned char c : value) {
|
|
bool unreserved = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
|
(c >= '0' && c <= '9') || c == '-' || c == '.' ||
|
|
c == '_' || c == '~';
|
|
if (unreserved) {
|
|
out.push_back(static_cast<char>(c));
|
|
} else {
|
|
char buf[4];
|
|
std::snprintf(buf, sizeof(buf), "%%%02X", c);
|
|
out.append(buf);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
std::string normalize_server_url(const std::string& server_url) {
|
|
std::string out = server_url;
|
|
while (!out.empty() && out.back() == '/') {
|
|
out.pop_back();
|
|
}
|
|
return out;
|
|
}
|
|
|
|
std::string build_auth_url(const std::string& server_url) {
|
|
return normalize_server_url(server_url) + "/Users/AuthenticateByName";
|
|
}
|
|
|
|
namespace {
|
|
// Fields requested for lists: overview, year and SortName (local sort after
|
|
// cleaning invisible characters). UserData is always returned for a user.
|
|
const char kListFields[] = "Overview,ProductionYear,SortName";
|
|
} // namespace
|
|
|
|
std::string build_current_user_url(const std::string& server_url) {
|
|
return normalize_server_url(server_url) + "/Users/Me";
|
|
}
|
|
|
|
std::string build_logout_url(const std::string& server_url) {
|
|
return normalize_server_url(server_url) + "/Sessions/Logout";
|
|
}
|
|
|
|
std::string build_resume_url(const std::string& server_url, const std::string& user_id,
|
|
int limit, bool legacy_route) {
|
|
const std::string base = normalize_server_url(server_url);
|
|
std::string url = legacy_route
|
|
? base + "/Users/" + url_encode(user_id) + "/Items/Resume?"
|
|
: base + "/UserItems/Resume?userId=" + url_encode(user_id) + "&";
|
|
url += "mediaTypes=Video&fields=" + std::string(kListFields);
|
|
if (limit > 0) {
|
|
url += "&limit=" + std::to_string(limit);
|
|
}
|
|
return url;
|
|
}
|
|
|
|
std::string build_views_url(const std::string& server_url, const std::string& user_id) {
|
|
return normalize_server_url(server_url) + "/Users/" + url_encode(user_id) + "/Views";
|
|
}
|
|
|
|
std::string build_items_url(const std::string& server_url, const std::string& user_id,
|
|
const ItemsQuery& query) {
|
|
std::string url = normalize_server_url(server_url) + "/Users/" +
|
|
url_encode(user_id) + "/Items?sortBy=" + url_encode(query.sort_by) +
|
|
(query.sort_descending ? "&sortOrder=Descending" : "&sortOrder=Ascending") +
|
|
"&fields=" + kListFields;
|
|
if (query.limit > 0) {
|
|
url += "&limit=" + std::to_string(query.limit);
|
|
}
|
|
if (!query.parent_id.empty()) {
|
|
url += "&parentId=" + url_encode(query.parent_id);
|
|
}
|
|
if (!query.include_item_types.empty()) {
|
|
url += "&includeItemTypes=" + url_encode(query.include_item_types);
|
|
}
|
|
if (query.recursive) {
|
|
url += "&recursive=true";
|
|
}
|
|
if (query.collapse_box_set_items.has_value()) {
|
|
url += *query.collapse_box_set_items ? "&collapseBoxSetItems=true"
|
|
: "&collapseBoxSetItems=false";
|
|
}
|
|
return url;
|
|
}
|
|
|
|
std::string build_seasons_url(const std::string& server_url, const std::string& series_id,
|
|
const std::string& user_id) {
|
|
return normalize_server_url(server_url) + "/Shows/" + url_encode(series_id) +
|
|
"/Seasons?userId=" + url_encode(user_id);
|
|
}
|
|
|
|
std::string build_episodes_url(const std::string& server_url, const std::string& series_id,
|
|
const std::string& season_id, const std::string& user_id) {
|
|
std::string url = normalize_server_url(server_url) + "/Shows/" +
|
|
url_encode(series_id) +
|
|
"/Episodes?userId=" + url_encode(user_id);
|
|
if (!season_id.empty()) {
|
|
url += "&seasonId=" + url_encode(season_id);
|
|
}
|
|
return url;
|
|
}
|
|
|
|
std::string build_image_url(const std::string& server_url, const std::string& item_id,
|
|
const std::string& image_tag, int max_width) {
|
|
std::string url = normalize_server_url(server_url) + "/Items/" +
|
|
url_encode(item_id) + "/Images/Primary";
|
|
char width[32];
|
|
std::snprintf(width, sizeof(width), "?maxWidth=%d&quality=90", max_width);
|
|
url += width;
|
|
if (!image_tag.empty()) {
|
|
url += "&tag=" + url_encode(image_tag);
|
|
}
|
|
return url;
|
|
}
|
|
|
|
std::string build_stream_url(const std::string& server_url, const std::string& item_id,
|
|
const std::string& access_token, const std::string& device_id,
|
|
long long start_time_ticks,
|
|
const std::string& play_session_id,
|
|
bool include_api_key, const TrackSelection& tracks) {
|
|
std::string url = normalize_server_url(server_url) + "/Videos/" +
|
|
url_encode(item_id) +
|
|
"/stream.ts"
|
|
"?static=false"
|
|
"&videoCodec=h264"
|
|
"&audioCodec=aac"
|
|
"&maxWidth=1920"
|
|
"&maxHeight=1080"
|
|
"&audioChannels=2"
|
|
"&videoBitRate=8000000"
|
|
"&audioBitRate=192000"
|
|
"&deviceId=" + url_encode(device_id);
|
|
if (include_api_key) {
|
|
url += "&api_key=" + url_encode(access_token);
|
|
}
|
|
if (start_time_ticks > 0) {
|
|
char ticks[32];
|
|
std::snprintf(ticks, sizeof(ticks), "%lld", start_time_ticks);
|
|
url += "&startTimeTicks=";
|
|
url += ticks;
|
|
}
|
|
if (!play_session_id.empty()) {
|
|
url += "&playSessionId=" + url_encode(play_session_id);
|
|
}
|
|
if (!tracks.media_source_id.empty()) {
|
|
url += "&mediaSourceId=" + url_encode(tracks.media_source_id);
|
|
}
|
|
if (tracks.audio_index >= 0) {
|
|
url += "&audioStreamIndex=" + std::to_string(tracks.audio_index);
|
|
}
|
|
if (tracks.subtitle_delivery == SubtitleDelivery::Encode && tracks.subtitle_index >= 0) {
|
|
url += "&subtitleStreamIndex=" + std::to_string(tracks.subtitle_index) +
|
|
"&subtitleMethod=Encode";
|
|
}
|
|
return url;
|
|
}
|
|
|
|
std::string build_subtitle_url(const std::string& server_url, const std::string& item_id,
|
|
const std::string& media_source_id, int stream_index) {
|
|
return normalize_server_url(server_url) + "/Videos/" + url_encode(item_id) + "/" +
|
|
url_encode(media_source_id.empty() ? item_id : media_source_id) + "/Subtitles/" +
|
|
std::to_string(stream_index) + "/Stream.srt";
|
|
}
|
|
|
|
std::string build_item_url(const std::string& server_url, const std::string& user_id,
|
|
const std::string& item_id) {
|
|
return normalize_server_url(server_url) + "/Users/" + url_encode(user_id) +
|
|
"/Items/" + url_encode(item_id);
|
|
}
|
|
|
|
std::string build_active_encodings_url(const std::string& server_url,
|
|
const std::string& device_id,
|
|
const std::string& play_session_id) {
|
|
return normalize_server_url(server_url) + "/Videos/ActiveEncodings?deviceId=" +
|
|
url_encode(device_id) + "&playSessionId=" + url_encode(play_session_id);
|
|
}
|
|
|
|
std::string mask_api_key(const std::string& url) {
|
|
const std::string key = "api_key=";
|
|
std::string out = url;
|
|
size_t pos = out.find(key);
|
|
if (pos != std::string::npos) {
|
|
size_t value_start = pos + key.size();
|
|
size_t value_end = out.find('&', value_start);
|
|
if (value_end == std::string::npos) {
|
|
value_end = out.size();
|
|
}
|
|
out.replace(value_start, value_end - value_start, "********");
|
|
}
|
|
return out;
|
|
}
|
|
|
|
} // namespace api
|