mirror of
https://github.com/steredbits/pelagia.git
synced 2026-10-17 21:01:34 +02:00
280 lines
9.8 KiB
C++
280 lines
9.8 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Copyright (C) 2026 steredbits and Pelagia contributors
|
|
#include "api/jellyfin_json.h"
|
|
|
|
#include <cjson/cJSON.h>
|
|
|
|
namespace api {
|
|
|
|
namespace {
|
|
|
|
// Copies a JSON string to out; leaves out untouched if absent or not a string.
|
|
void read_string(const cJSON* obj, const char* key, std::string* out) {
|
|
const cJSON* v = cJSON_GetObjectItemCaseSensitive(obj, key);
|
|
if (cJSON_IsString(v) && v->valuestring) {
|
|
*out = v->valuestring;
|
|
}
|
|
}
|
|
|
|
void read_int(const cJSON* obj, const char* key, int* out) {
|
|
const cJSON* v = cJSON_GetObjectItemCaseSensitive(obj, key);
|
|
if (cJSON_IsNumber(v)) {
|
|
*out = v->valueint;
|
|
}
|
|
}
|
|
|
|
void read_int64(const cJSON* obj, const char* key, long long* out) {
|
|
const cJSON* v = cJSON_GetObjectItemCaseSensitive(obj, key);
|
|
if (cJSON_IsNumber(v)) {
|
|
*out = static_cast<long long>(v->valuedouble);
|
|
}
|
|
}
|
|
|
|
void read_bool(const cJSON* obj, const char* key, bool* out) {
|
|
const cJSON* v = cJSON_GetObjectItemCaseSensitive(obj, key);
|
|
if (cJSON_IsBool(v)) {
|
|
*out = cJSON_IsTrue(v);
|
|
}
|
|
}
|
|
|
|
// Enumeration serialized as text ("Smart"); a number is also accepted
|
|
// (server enumeration order: Default, Always, OnlyForced, None, Smart).
|
|
SubtitleMode parse_subtitle_mode(const cJSON* v) {
|
|
if (cJSON_IsNumber(v)) {
|
|
switch (v->valueint) {
|
|
case 1: return SubtitleMode::Always;
|
|
case 2: return SubtitleMode::OnlyForced;
|
|
case 3: return SubtitleMode::None;
|
|
case 4: return SubtitleMode::Smart;
|
|
default: return SubtitleMode::Default;
|
|
}
|
|
}
|
|
if (cJSON_IsString(v) && v->valuestring) {
|
|
const std::string name = v->valuestring;
|
|
if (name == "Always") return SubtitleMode::Always;
|
|
if (name == "OnlyForced") return SubtitleMode::OnlyForced;
|
|
if (name == "None") return SubtitleMode::None;
|
|
if (name == "Smart") return SubtitleMode::Smart;
|
|
}
|
|
return SubtitleMode::Default;
|
|
}
|
|
|
|
void parse_preferences(const cJSON* user, UserPreferences* out) {
|
|
const cJSON* cfg = cJSON_GetObjectItemCaseSensitive(user, "Configuration");
|
|
if (!cJSON_IsObject(cfg)) {
|
|
return;
|
|
}
|
|
read_string(cfg, "AudioLanguagePreference", &out->audio_language);
|
|
read_bool(cfg, "PlayDefaultAudioTrack", &out->play_default_audio);
|
|
read_string(cfg, "SubtitleLanguagePreference", &out->subtitle_language);
|
|
out->subtitle_mode = parse_subtitle_mode(cJSON_GetObjectItemCaseSensitive(cfg, "SubtitleMode"));
|
|
read_bool(cfg, "RememberAudioSelections", &out->remember_audio);
|
|
read_bool(cfg, "RememberSubtitleSelections", &out->remember_subtitles);
|
|
}
|
|
|
|
StreamKind parse_stream_kind(const cJSON* obj) {
|
|
const cJSON* v = cJSON_GetObjectItemCaseSensitive(obj, "Type");
|
|
if (cJSON_IsString(v) && v->valuestring) {
|
|
const std::string type = v->valuestring;
|
|
if (type == "Audio") return StreamKind::Audio;
|
|
if (type == "Subtitle") return StreamKind::Subtitle;
|
|
if (type == "Video") return StreamKind::Video;
|
|
}
|
|
return StreamKind::Other;
|
|
}
|
|
|
|
void parse_media_stream(const cJSON* obj, MediaStreamInfo* out) {
|
|
out->kind = parse_stream_kind(obj);
|
|
read_int(obj, "Index", &out->index);
|
|
read_string(obj, "Codec", &out->codec);
|
|
read_string(obj, "Language", &out->language);
|
|
read_string(obj, "Title", &out->title);
|
|
read_string(obj, "DisplayTitle", &out->display_title);
|
|
read_string(obj, "ChannelLayout", &out->channel_layout);
|
|
read_int(obj, "Channels", &out->channels);
|
|
read_bool(obj, "IsDefault", &out->is_default);
|
|
read_bool(obj, "IsForced", &out->is_forced);
|
|
read_bool(obj, "IsExternal", &out->is_external);
|
|
read_bool(obj, "IsHearingImpaired", &out->is_hearing_impaired);
|
|
read_bool(obj, "IsOriginal", &out->is_original);
|
|
read_bool(obj, "IsTextSubtitleStream", &out->is_text);
|
|
read_bool(obj, "SupportsExternalStream", &out->supports_external);
|
|
}
|
|
|
|
void parse_media_source(const cJSON* obj, MediaSourceInfo* out) {
|
|
read_string(obj, "Id", &out->id);
|
|
read_string(obj, "Container", &out->container);
|
|
const cJSON* audio = cJSON_GetObjectItemCaseSensitive(obj, "DefaultAudioStreamIndex");
|
|
const cJSON* sub = cJSON_GetObjectItemCaseSensitive(obj, "DefaultSubtitleStreamIndex");
|
|
out->server_defaults = cJSON_IsNumber(audio) || cJSON_IsNumber(sub);
|
|
if (cJSON_IsNumber(audio)) out->default_audio_index = audio->valueint;
|
|
if (cJSON_IsNumber(sub)) out->default_subtitle_index = sub->valueint;
|
|
const cJSON* streams = cJSON_GetObjectItemCaseSensitive(obj, "MediaStreams");
|
|
const cJSON* entry = nullptr;
|
|
cJSON_ArrayForEach(entry, streams) {
|
|
MediaStreamInfo info;
|
|
parse_media_stream(entry, &info);
|
|
if (info.index >= 0 && info.kind != StreamKind::Other) {
|
|
out->streams.push_back(info);
|
|
}
|
|
}
|
|
}
|
|
|
|
void parse_media_item(const cJSON* obj, MediaItem* out) {
|
|
read_string(obj, "Id", &out->id);
|
|
read_string(obj, "Name", &out->name);
|
|
read_string(obj, "Type", &out->type);
|
|
read_string(obj, "Overview", &out->overview);
|
|
read_string(obj, "SeriesName", &out->series_name);
|
|
read_string(obj, "SeriesId", &out->series_id);
|
|
read_string(obj, "SeriesPrimaryImageTag", &out->series_primary_image_tag);
|
|
read_string(obj, "SortName", &out->sort_name);
|
|
read_int(obj, "ProductionYear", &out->production_year);
|
|
read_int(obj, "IndexNumber", &out->index_number);
|
|
read_int(obj, "ParentIndexNumber", &out->parent_index_number);
|
|
read_int64(obj, "RunTimeTicks", &out->runtime_ticks);
|
|
|
|
const cJSON* tags = cJSON_GetObjectItemCaseSensitive(obj, "ImageTags");
|
|
if (cJSON_IsObject(tags)) {
|
|
read_string(tags, "Primary", &out->primary_image_tag);
|
|
}
|
|
|
|
const cJSON* user_data = cJSON_GetObjectItemCaseSensitive(obj, "UserData");
|
|
if (cJSON_IsObject(user_data)) {
|
|
read_int64(user_data, "PlaybackPositionTicks", &out->playback_position_ticks);
|
|
out->played = cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(user_data, "Played"));
|
|
}
|
|
|
|
const cJSON* sources = cJSON_GetObjectItemCaseSensitive(obj, "MediaSources");
|
|
if (cJSON_IsArray(sources) && cJSON_GetArraySize(sources) > 0) {
|
|
read_string(cJSON_GetArrayItem(sources, 0), "Id", &out->media_source_id);
|
|
out->media_sources.clear(); // out may be an already read item (re-read)
|
|
const cJSON* source = nullptr;
|
|
cJSON_ArrayForEach(source, sources) {
|
|
MediaSourceInfo info;
|
|
parse_media_source(source, &info);
|
|
out->media_sources.push_back(info);
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool parse_item_response(const std::string& json, MediaItem* out) {
|
|
cJSON* root = cJSON_Parse(json.c_str());
|
|
if (!root) {
|
|
return false;
|
|
}
|
|
bool ok = cJSON_IsObject(root);
|
|
if (ok) {
|
|
parse_media_item(root, out);
|
|
ok = !out->id.empty();
|
|
}
|
|
cJSON_Delete(root);
|
|
return ok;
|
|
}
|
|
|
|
bool parse_auth_response(const std::string& json, AuthSession* out) {
|
|
cJSON* root = cJSON_Parse(json.c_str());
|
|
if (!root) {
|
|
return false;
|
|
}
|
|
|
|
bool ok = false;
|
|
read_string(root, "AccessToken", &out->access_token);
|
|
read_string(root, "ServerId", &out->server_id);
|
|
const cJSON* user = cJSON_GetObjectItemCaseSensitive(root, "User");
|
|
if (cJSON_IsObject(user)) {
|
|
read_string(user, "Id", &out->user_id);
|
|
read_string(user, "Name", &out->user_name);
|
|
parse_preferences(user, &out->preferences);
|
|
}
|
|
ok = !out->access_token.empty() && !out->user_id.empty();
|
|
|
|
cJSON_Delete(root);
|
|
return ok;
|
|
}
|
|
|
|
bool parse_user_response(const std::string& json, AuthSession* out) {
|
|
cJSON* root = cJSON_Parse(json.c_str());
|
|
if (!root) {
|
|
return false;
|
|
}
|
|
read_string(root, "Id", &out->user_id);
|
|
read_string(root, "Name", &out->user_name);
|
|
read_string(root, "ServerId", &out->server_id);
|
|
parse_preferences(root, &out->preferences);
|
|
const bool ok = cJSON_IsObject(root) && !out->user_id.empty();
|
|
cJSON_Delete(root);
|
|
return ok;
|
|
}
|
|
|
|
bool parse_views_response(const std::string& json, std::vector<Library>* out) {
|
|
cJSON* root = cJSON_Parse(json.c_str());
|
|
if (!root) {
|
|
return false;
|
|
}
|
|
|
|
const cJSON* items = cJSON_GetObjectItemCaseSensitive(root, "Items");
|
|
bool ok = cJSON_IsArray(items);
|
|
if (ok) {
|
|
const cJSON* entry = nullptr;
|
|
cJSON_ArrayForEach(entry, items) {
|
|
Library lib;
|
|
read_string(entry, "Id", &lib.id);
|
|
read_string(entry, "Name", &lib.name);
|
|
read_string(entry, "CollectionType", &lib.collection_type);
|
|
if (!lib.id.empty()) {
|
|
out->push_back(lib);
|
|
}
|
|
}
|
|
}
|
|
|
|
cJSON_Delete(root);
|
|
return ok;
|
|
}
|
|
|
|
bool parse_items_response(const std::string& json, ItemList* out) {
|
|
cJSON* root = cJSON_Parse(json.c_str());
|
|
if (!root) {
|
|
return false;
|
|
}
|
|
|
|
const cJSON* items = cJSON_GetObjectItemCaseSensitive(root, "Items");
|
|
bool ok = cJSON_IsArray(items);
|
|
if (ok) {
|
|
const cJSON* entry = nullptr;
|
|
cJSON_ArrayForEach(entry, items) {
|
|
MediaItem item;
|
|
parse_media_item(entry, &item);
|
|
if (!item.id.empty()) {
|
|
out->items.push_back(item);
|
|
}
|
|
}
|
|
out->total_count = static_cast<int>(out->items.size());
|
|
read_int(root, "TotalRecordCount", &out->total_count);
|
|
}
|
|
|
|
cJSON_Delete(root);
|
|
return ok;
|
|
}
|
|
|
|
std::string build_auth_body(const std::string& username, const std::string& password) {
|
|
std::string body;
|
|
cJSON* root = cJSON_CreateObject();
|
|
if (!root) {
|
|
return body;
|
|
}
|
|
cJSON_AddStringToObject(root, "Username", username.c_str());
|
|
cJSON_AddStringToObject(root, "Pw", password.c_str());
|
|
char* printed = cJSON_PrintUnformatted(root);
|
|
if (printed) {
|
|
body = printed;
|
|
cJSON_free(printed);
|
|
}
|
|
cJSON_Delete(root);
|
|
return body;
|
|
}
|
|
|
|
} // namespace api
|