v0.2: All videos folder, shared watched state, release-order sorting, official icon, OAuth plumbing, docs
Some checks failed
release / build-release (push) Has been cancelled
Some checks failed
release / build-release (push) Has been cancelled
- per-creator "All videos" folder (release order across sub-channels); sub-channel copies use cpost: ids and UserDataMirror keeps played/resume state in sync - SortNameUpdater forces release-order sort names so every client lists newest first by default - official Floatplane icon as channel image (Primary only — a channel Thumb hid video thumbnails in Continue Watching) and as 16:9 plugin thumbnail; one-time image replacement on upgrade - OAuth device-flow login (FloatplaneAuthService + settings UI); needs a Floatplane client id; Turnstile verified domain-bound so password login inside Jellyfin is impossible - generic install.sh (linuxserver / official image / native), thumb.png in package + manifest - README rewritten for a general audience, doc/usage.md with screenshots, 60 tests Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -32,9 +32,27 @@ public class PluginConfiguration : BasePluginConfiguration
|
||||
/// <summary>Highest level offered as a version at all (keeps 4K out of the list by default).</summary>
|
||||
public int MaxHeight { get; set; } = 1080;
|
||||
|
||||
/// <summary>How many posts (newest first) are listed per Floatplane channel.</summary>
|
||||
/// <summary>How many posts (newest first) are listed per Floatplane sub-channel.</summary>
|
||||
public int MaxPostsPerChannel { get; set; } = 100;
|
||||
|
||||
/// <summary>How many posts (newest first) the per-creator "All videos" folder lists.</summary>
|
||||
public int MaxPostsAllVideos { get; set; } = 200;
|
||||
|
||||
/// <summary>
|
||||
/// OAuth client id registered with Floatplane for this plugin. Empty = OAuth login
|
||||
/// unavailable (use the cookie). See the README for how to obtain one.
|
||||
/// </summary>
|
||||
public string OAuthClientId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>OAuth access token (short-lived, refreshed automatically).</summary>
|
||||
public string OAuthAccessToken { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>OAuth refresh token (≈90 days, rotated on refresh). Presence = logged in via OAuth.</summary>
|
||||
public string OAuthRefreshToken { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Unix time (seconds) when the access token expires.</summary>
|
||||
public long OAuthAccessTokenExpiry { get; set; }
|
||||
|
||||
/// <summary>Show posts the account cannot access (higher tier) as locked items.</summary>
|
||||
public bool ShowLockedPosts { get; set; }
|
||||
|
||||
@@ -59,6 +77,9 @@ public class PluginConfiguration : BasePluginConfiguration
|
||||
/// <summary>Hours between automatic catalogue refreshes (scheduled task).</summary>
|
||||
public int SyncIntervalHours { get; set; } = 6;
|
||||
|
||||
/// <summary>Version of the channel logo Jellyfin last stored (internal; drives a one-time image refresh).</summary>
|
||||
public int ChannelImageVersion { get; set; }
|
||||
|
||||
/// <summary>Report watch progress back to Floatplane (not implemented yet).</summary>
|
||||
public bool SyncProgressToFloatplane { get; set; }
|
||||
}
|
||||
|
||||
@@ -30,6 +30,23 @@
|
||||
<button is="emby-button" type="button" id="btnValidate" class="raised"><span>Validate cookie</span></button>
|
||||
<span id="validateResult" class="fieldDescription" style="margin-left:1em"></span>
|
||||
</div>
|
||||
|
||||
<h3 class="sectionTitle" style="margin-top:1.5em">Login with Floatplane (OAuth, optional)</h3>
|
||||
<div class="fieldDescription" style="margin-bottom:0.5em">
|
||||
Floatplane's own apps and third-party clients log in through an OAuth device flow instead of a cookie.
|
||||
That needs a <b>client id</b> issued by Floatplane for this plugin — there is no self-service registration.
|
||||
If you have one, enter it and press "Login with Floatplane"; the OAuth session then replaces the cookie
|
||||
(refresh tokens last about 90 days and renew themselves).
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="OAuthClientId">OAuth client id</label>
|
||||
<input is="emby-input" type="text" id="OAuthClientId" placeholder="(none)" />
|
||||
</div>
|
||||
<div>
|
||||
<button is="emby-button" type="button" id="btnOAuth" class="raised"><span>Login with Floatplane</span></button>
|
||||
<button is="emby-button" type="button" id="btnOAuthLogout" class="raised" hidden><span>Forget OAuth session</span></button>
|
||||
<div id="oauthResult" class="fieldDescription" style="margin-top:0.5em"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="verticalSection">
|
||||
@@ -40,9 +57,14 @@
|
||||
<div class="fieldDescription">Shown in addition to your subscriptions — or instead of them when no cookie is set (public listings only, no playback).</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="MaxPostsPerChannel">Videos listed per channel</label>
|
||||
<label class="inputLabel" for="MaxPostsAllVideos">Videos listed in "All videos"</label>
|
||||
<input is="emby-input" type="number" id="MaxPostsAllVideos" min="20" max="5000" step="20" />
|
||||
<div class="fieldDescription">Each creator gets an "All videos" folder with every channel's videos in release order, newest first. Floatplane pages 20 at a time, so 200 = 10 requests.</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel" for="MaxPostsPerChannel">Videos listed per sub-channel</label>
|
||||
<input is="emby-input" type="number" id="MaxPostsPerChannel" min="20" max="2000" step="20" />
|
||||
<div class="fieldDescription">Newest first. Floatplane pages 20 at a time, so 100 = 5 requests per channel.</div>
|
||||
<div class="fieldDescription">Newest first, per sub-channel folder (TechLinked, ShortCircuit, …).</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
@@ -126,8 +148,8 @@
|
||||
<script type="text/javascript">
|
||||
(function () {
|
||||
var pluginId = "7f2b6c1e-5a3d-4e8f-9b0a-2c4d6e8f0a1b";
|
||||
var fields = ["SessionCookie", "ExtraCreators", "MaxPostsPerChannel", "CatalogCacheMinutes", "SyncIntervalHours",
|
||||
"PreferredQuality", "MaxHeight", "DeliveryCallsPerMinute", "ProxyBaseUrl"];
|
||||
var fields = ["SessionCookie", "ExtraCreators", "MaxPostsPerChannel", "MaxPostsAllVideos", "CatalogCacheMinutes", "SyncIntervalHours",
|
||||
"PreferredQuality", "MaxHeight", "DeliveryCallsPerMinute", "ProxyBaseUrl", "OAuthClientId"];
|
||||
var checks = ["ShowLockedPosts", "EnableMp4Sources"];
|
||||
|
||||
function el(id) { return document.getElementById(id); }
|
||||
@@ -138,15 +160,17 @@
|
||||
ApiClient.getJSON(ApiClient.getUrl("Floatplane/Status")).then(function (s) {
|
||||
var lines = [];
|
||||
lines.push("Plugin " + esc(s.Version));
|
||||
var kind = s.SessionKind === "oauth" ? "OAuth" : "cookie";
|
||||
if (!s.HasSessionCookie) {
|
||||
lines.push("Session: <b>no cookie</b> — browsing only.");
|
||||
lines.push("Session: <b>none</b> — browsing only (public listings); paste a cookie or log in to play.");
|
||||
} else if (s.SessionState === "ok") {
|
||||
lines.push("Session: <b>ok</b>" + (s.SessionUsername ? " (" + esc(s.SessionUsername) + ")" : ""));
|
||||
lines.push("Session: <b>ok</b> via " + kind + (s.SessionUsername ? " (" + esc(s.SessionUsername) + ")" : ""));
|
||||
} else if (s.SessionState === "invalid") {
|
||||
lines.push("Session: <b style='color:#e55'>rejected</b> — paste a fresh cookie. " + esc(s.LastError || ""));
|
||||
lines.push("Session (" + kind + "): <b style='color:#e55'>rejected</b> — " + (s.SessionKind === "oauth" ? "log in again." : "paste a fresh cookie. ") + esc(s.LastError || ""));
|
||||
} else {
|
||||
lines.push("Session: cookie set, not checked yet.");
|
||||
lines.push("Session: " + kind + " configured, not checked yet.");
|
||||
}
|
||||
el("btnOAuthLogout").hidden = s.SessionKind !== "oauth";
|
||||
lines.push("Creators: " + s.Creators.length + (s.LastRefresh ? " (refreshed " + new Date(s.LastRefresh).toLocaleString() + ")" : " (not loaded yet)"));
|
||||
s.Creators.forEach(function (c) { lines.push(" • " + esc(c.Title) + " — " + esc(c.Channels.join(", "))); });
|
||||
lines.push("Cached post lists: " + s.CachedPostLists + ", cached deliveries: " + s.CachedDeliveries + ", generation " + s.Generation);
|
||||
@@ -213,6 +237,44 @@
|
||||
});
|
||||
}
|
||||
|
||||
var oauthTimer = null;
|
||||
function oauthStart() {
|
||||
var out = el("oauthResult");
|
||||
var clientId = el("OAuthClientId").value.trim();
|
||||
if (!clientId) { out.textContent = "Enter the client id Floatplane issued for this plugin first."; return; }
|
||||
out.textContent = "Contacting auth.floatplane.com…";
|
||||
ApiClient.ajax({
|
||||
type: "POST", url: ApiClient.getUrl("Floatplane/OAuth/Start"),
|
||||
data: JSON.stringify({ ClientId: clientId }), contentType: "application/json", dataType: "json"
|
||||
}).then(function (r) {
|
||||
var link = r.VerificationUriComplete || r.VerificationUri;
|
||||
out.innerHTML = "Open <a href='" + esc(link) + "' target='_blank' rel='noopener'>" + esc(link) + "</a> and enter code <b style='font-size:1.3em'>" + esc(r.UserCode) + "</b> (valid " + Math.round(r.ExpiresIn / 60) + " min). Waiting…";
|
||||
var interval = (r.Interval || 5) * 1000;
|
||||
clearInterval(oauthTimer);
|
||||
oauthTimer = setInterval(function () {
|
||||
ApiClient.getJSON(ApiClient.getUrl("Floatplane/OAuth/Poll")).then(function (s) {
|
||||
if (s.State === "pending") { return; }
|
||||
clearInterval(oauthTimer);
|
||||
out.textContent = s.Message || s.State;
|
||||
out.style.color = s.State === "done" ? "#4c4" : "#e55";
|
||||
if (s.State === "done") { loadStatus(); }
|
||||
}).catch(function () { clearInterval(oauthTimer); out.textContent = "Polling failed."; });
|
||||
}, interval);
|
||||
}).catch(function (err) {
|
||||
var msg = "Start failed";
|
||||
try { msg += ": " + (err.responseText || ("HTTP " + err.status)); } catch (e) { /* ignore */ }
|
||||
out.textContent = msg + " — usually the client id is not registered with Floatplane (invalid_client).";
|
||||
out.style.color = "#e55";
|
||||
});
|
||||
}
|
||||
|
||||
function oauthLogout() {
|
||||
ApiClient.ajax({ type: "POST", url: ApiClient.getUrl("Floatplane/OAuth/Logout") }).then(function () {
|
||||
el("oauthResult").textContent = "OAuth session forgotten.";
|
||||
loadStatus();
|
||||
});
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
el("fpStatus").textContent = "Refreshing…";
|
||||
ApiClient.ajax({ type: "POST", url: ApiClient.getUrl("Floatplane/Refresh") }).then(loadStatus, loadStatus);
|
||||
@@ -238,6 +300,8 @@
|
||||
el("FloatplaneConfigForm").addEventListener("submit", saveConfig);
|
||||
el("btnValidate").addEventListener("click", validate);
|
||||
el("btnRefresh").addEventListener("click", refresh);
|
||||
el("btnOAuth").addEventListener("click", oauthStart);
|
||||
el("btnOAuthLogout").addEventListener("click", oauthLogout);
|
||||
el("btnSearch").addEventListener("click", search);
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user