mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-19 19:14:51 +00:00
Basic History backup & export, Minor pooling cleanup, some docs
This commit is contained in:
parent
7ebd8f13c2
commit
b1aae244de
8 changed files with 424 additions and 37 deletions
|
@ -6,6 +6,7 @@ import com.futo.platformplayer.getOrDefault
|
|||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.getOrThrowNullable
|
||||
import com.futo.polycentric.core.combineHashCodes
|
||||
import okhttp3.internal.platform.Platform
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class PlatformID {
|
||||
|
@ -40,6 +41,8 @@ class PlatformID {
|
|||
}
|
||||
|
||||
companion object {
|
||||
val NONE = PlatformID("Unknown", null);
|
||||
|
||||
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformID {
|
||||
val contextName = "PlatformID";
|
||||
return PlatformID(
|
||||
|
@ -49,5 +52,9 @@ class PlatformID {
|
|||
value.getOrDefault(config, "claimType", contextName, 0) ?: 0,
|
||||
value.getOrDefault(config, "claimFieldType", contextName, -1) ?: -1);
|
||||
}
|
||||
|
||||
fun asUrlID(url: String): PlatformID {
|
||||
return PlatformID("URL", url, null);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package com.futo.platformplayer.api.media
|
||||
|
||||
class PlatformMultiClientPool {
|
||||
private val _maxCap: Int;
|
||||
private val _clientPools: HashMap<IPlatformClient, PlatformClientPool> = hashMapOf();
|
||||
|
||||
constructor(maxCap: Int = -1) {
|
||||
_maxCap = if(maxCap > 0)
|
||||
maxCap
|
||||
else 99;
|
||||
}
|
||||
|
||||
fun getClientPooled(parentClient: IPlatformClient, capacity: Int): IPlatformClient {
|
||||
val pool = synchronized(_clientPools) {
|
||||
if(!_clientPools.containsKey(parentClient))
|
||||
_clientPools[parentClient] = PlatformClientPool(parentClient).apply {
|
||||
this.onDead.subscribe { client, pool ->
|
||||
synchronized(_clientPools) {
|
||||
if(_clientPools[parentClient] == pool)
|
||||
_clientPools.remove(parentClient);
|
||||
}
|
||||
}
|
||||
}
|
||||
_clientPools[parentClient]!!;
|
||||
};
|
||||
return pool.getClient(capacity.coerceAtMost(_maxCap));
|
||||
}
|
||||
}
|
|
@ -1,8 +1,13 @@
|
|||
package com.futo.platformplayer.models
|
||||
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||
import java.time.LocalDateTime
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class HistoryVideo {
|
||||
|
@ -18,4 +23,41 @@ class HistoryVideo {
|
|||
this.position = position;
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
|
||||
fun toReconString(): String {
|
||||
return "${video.url}|||${date.toEpochSecond()}|||${position}|||${video.name}";
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromReconString(str: String, resolve: ((url: String)->SerializedPlatformVideo)? = null): HistoryVideo {
|
||||
var index = str.indexOf("|||");
|
||||
if(index < 0) throw IllegalArgumentException("Invalid history string: " + str);
|
||||
val url = str.substring(0, index);
|
||||
|
||||
var indexNext = str.indexOf("|||", index + 3);
|
||||
if(indexNext < 0) throw IllegalArgumentException("Invalid history string: " + str);
|
||||
val dateSec = str.substring(index + 3, indexNext).toLong();
|
||||
|
||||
index = indexNext;
|
||||
indexNext = str.indexOf("|||", index + 3);
|
||||
if(indexNext < 0) throw IllegalArgumentException("Invalid history string: " + str);
|
||||
val position = str.substring(index + 3, indexNext).toLong();
|
||||
val name = str.substring(indexNext + 3);
|
||||
|
||||
val video = resolve?.invoke(url) ?: SerializedPlatformVideo(
|
||||
id = PlatformID.asUrlID(url),
|
||||
name = name,
|
||||
thumbnails = Thumbnails(),
|
||||
author = PlatformAuthorLink(PlatformID.NONE, "Unknown", ""),
|
||||
datetime = null,
|
||||
url = url,
|
||||
shareUrl = url,
|
||||
duration = 0,
|
||||
viewCount = -1
|
||||
);
|
||||
|
||||
return HistoryVideo(video, position, OffsetDateTime.of(LocalDateTime.ofEpochSecond(dateSec, 0, ZoneOffset.UTC), ZoneOffset.UTC));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ import com.futo.platformplayer.UIDialogs
|
|||
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.activities.SettingsActivity
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.encryption.EncryptionProvider
|
||||
import com.futo.platformplayer.getNowDiffHours
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
|
@ -201,7 +202,8 @@ class StateBackup {
|
|||
);
|
||||
val storesToSave = getAllMigrationStores()
|
||||
.associateBy { it.name }
|
||||
.mapValues { it.value.getAllReconstructionStrings() };
|
||||
.mapValues { it.value.getAllReconstructionStrings() }
|
||||
.toMutableMap();
|
||||
val settings = Settings.instance.encode();
|
||||
val pluginSettings = StatePlugins.instance.getPlugins()
|
||||
.associateBy { it.config.id }
|
||||
|
@ -211,7 +213,12 @@ class StateBackup {
|
|||
.associateBy { it.config.id }
|
||||
.mapValues { it.value.config.sourceUrl!! };
|
||||
|
||||
return ExportStructure(exportInfo, settings, storesToSave, pluginUrls, pluginSettings);
|
||||
|
||||
val export = ExportStructure(exportInfo, settings, storesToSave, pluginUrls, pluginSettings);
|
||||
//export.videoCache = StatePlaylists.instance.getHistory()
|
||||
// .distinctBy { it.video.url }
|
||||
// .map { it.video };
|
||||
return export;
|
||||
}
|
||||
|
||||
|
||||
|
@ -387,6 +394,7 @@ class StateBackup {
|
|||
val plugins: Map<String, String>,
|
||||
val pluginSettings: Map<String, Map<String, String?>>,
|
||||
) {
|
||||
var videoCache: List<SerializedPlatformVideo>? = null;
|
||||
|
||||
fun asZip(): ByteArray {
|
||||
return ByteArrayOutputStream().use { byteStream ->
|
||||
|
|
|
@ -8,6 +8,7 @@ import com.futo.platformplayer.UIDialogs
|
|||
import com.futo.platformplayer.api.media.IPlatformClient
|
||||
import com.futo.platformplayer.api.media.IPluginSourced
|
||||
import com.futo.platformplayer.api.media.PlatformClientPool
|
||||
import com.futo.platformplayer.api.media.PlatformMultiClientPool
|
||||
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
||||
import com.futo.platformplayer.api.media.models.FilterGroup
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
|
@ -38,6 +39,7 @@ import com.futo.platformplayer.stores.*
|
|||
import kotlinx.coroutines.*
|
||||
import okhttp3.internal.concat
|
||||
import java.time.OffsetDateTime
|
||||
import kotlin.reflect.jvm.internal.impl.builtins.jvm.JavaToKotlinClassMap.PlatformMutabilityMapping
|
||||
import kotlin.streams.toList
|
||||
|
||||
/***
|
||||
|
@ -61,8 +63,8 @@ class StatePlatform {
|
|||
private val _availableClients : ArrayList<IPlatformClient> = ArrayList();
|
||||
private val _enabledClients : ArrayList<IPlatformClient> = ArrayList();
|
||||
|
||||
private val _clientPools: HashMap<IPlatformClient, PlatformClientPool> = hashMapOf();
|
||||
private val _trackerClientPools: HashMap<IPlatformClient, PlatformClientPool> = hashMapOf();
|
||||
private val _channelClientPool = PlatformMultiClientPool(15);
|
||||
private val _trackerClientPool = PlatformMultiClientPool(1);
|
||||
|
||||
private val _primaryClientPersistent = FragmentedStorage.get<StringStorage>("primaryClient");
|
||||
private var _primaryClientObj : IPlatformClient? = null;
|
||||
|
@ -233,36 +235,6 @@ class StatePlatform {
|
|||
fun getClient(id: String): IPlatformClient {
|
||||
return getClientOrNull(id) ?: throw IllegalArgumentException("Client with id $id does not exist");
|
||||
}
|
||||
fun getClientPooled(parentClient: IPlatformClient, capacity: Int): IPlatformClient {
|
||||
val pool = synchronized(_clientPools) {
|
||||
if(!_clientPools.containsKey(parentClient))
|
||||
_clientPools[parentClient] = PlatformClientPool(parentClient).apply {
|
||||
this.onDead.subscribe { client, pool ->
|
||||
synchronized(_clientPools) {
|
||||
if(_clientPools[parentClient] == pool)
|
||||
_clientPools.remove(parentClient);
|
||||
}
|
||||
}
|
||||
}
|
||||
_clientPools[parentClient]!!;
|
||||
};
|
||||
return pool.getClient(capacity);
|
||||
}
|
||||
fun getTrackerClientPooled(parentClient: IPlatformClient, capacity: Int): IPlatformClient {
|
||||
val pool = synchronized(_trackerClientPools) {
|
||||
if(!_trackerClientPools.containsKey(parentClient))
|
||||
_trackerClientPools[parentClient] = PlatformClientPool(parentClient).apply {
|
||||
this.onDead.subscribe { client, pool ->
|
||||
synchronized(_trackerClientPools) {
|
||||
if(_trackerClientPools[parentClient] == pool)
|
||||
_trackerClientPools.remove(parentClient);
|
||||
}
|
||||
}
|
||||
}
|
||||
_trackerClientPools[parentClient]!!;
|
||||
};
|
||||
return pool.getClient(capacity);
|
||||
}
|
||||
|
||||
fun getClientsByClaimType(claimType: Int): List<IPlatformClient> {
|
||||
return getEnabledClients().filter { it.isClaimTypeSupported(claimType) };
|
||||
|
@ -627,7 +599,7 @@ class StatePlatform {
|
|||
if (baseClient !is JSClient) {
|
||||
return baseClient.getPlaybackTracker(url);
|
||||
}
|
||||
val client = getTrackerClientPooled(baseClient, 1);
|
||||
val client = _trackerClientPool.getClientPooled(baseClient, 1);
|
||||
return client.getPlaybackTracker(url);
|
||||
}
|
||||
|
||||
|
@ -651,7 +623,7 @@ class StatePlatform {
|
|||
val clientCapabilities = baseClient.getChannelCapabilities();
|
||||
|
||||
val client = if(usePooledClients > 1)
|
||||
getClientPooled(baseClient, usePooledClients);
|
||||
_channelClientPool.getClientPooled(baseClient, usePooledClients);
|
||||
else baseClient;
|
||||
|
||||
var lastStream: OffsetDateTime? = null;
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||
import android.net.Uri
|
||||
import androidx.core.content.FileProvider
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
|
@ -38,6 +39,11 @@ class StatePlaylists {
|
|||
})
|
||||
.load();
|
||||
private val _historyStore = FragmentedStorage.storeJson<HistoryVideo>("history")
|
||||
.withRestore(object: ReconstructStore<HistoryVideo>() {
|
||||
override fun toReconstruction(obj: HistoryVideo): String = obj.toReconString();
|
||||
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): HistoryVideo
|
||||
= HistoryVideo.fromReconString(backup, null);
|
||||
})
|
||||
.load();
|
||||
val playlistStore = FragmentedStorage.storeJson<Playlist>("playlists")
|
||||
.withRestore(PlaylistBackup())
|
||||
|
@ -49,7 +55,7 @@ class StatePlaylists {
|
|||
val onWatchLaterChanged = Event0();
|
||||
|
||||
fun toMigrateCheck(): List<ManagedStore<*>> {
|
||||
return listOf(playlistStore, _watchlistStore);
|
||||
return listOf(playlistStore, _watchlistStore, _historyStore);
|
||||
}
|
||||
|
||||
fun getWatchLater() : List<SerializedPlatformVideo> {
|
||||
|
@ -122,6 +128,11 @@ class StatePlaylists {
|
|||
}
|
||||
|
||||
if (shouldUpdate) {
|
||||
|
||||
//A unrecovered item
|
||||
if(historyVideo.video.author.id.value == null && historyVideo.video.duration == 0L)
|
||||
historyVideo.video = SerializedPlatformVideo.fromVideo(video);
|
||||
|
||||
historyVideo.position = pos;
|
||||
historyVideo.date = OffsetDateTime.now();
|
||||
_historyStore.saveAsync(historyVideo);
|
||||
|
|
279
docs/Content Types.md
Normal file
279
docs/Content Types.md
Normal file
|
@ -0,0 +1,279 @@
|
|||
# Content Types
|
||||
This page will cover the various types of content that are supported, and how to present them to Grayjay.
|
||||
|
||||
While Grayjay is primarily used for video, it supports various types of video, audio, but also text, images, and articles. In the future more types of content support might be added!
|
||||
|
||||
Content can be presented as a feed object, or a detail object. Feed objects are objects you see inside feeds and overviews such as the Home and Subscription tabs. Generally detail objects have an accompanying overview object.
|
||||
|
||||
Feed items are often returned in pagers, the following are some plugin methods that expect a pager of feed items:
|
||||
```
|
||||
source.getHome()
|
||||
source.getChannelContents(...)
|
||||
```
|
||||
Content details are generally retrieved using
|
||||
```
|
||||
source.getContentDetails(url)
|
||||
```
|
||||
|
||||
Note that all detail objects can be considered feed objects, but not the other way around. When you return a detail object in places where feed object is expected, and the user tries to open said item in a detail view, the ```GetContentDetail``` call is skipped, and the item is immediately shown without loading details.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Feed Types
|
||||
Feed types represent content in a feed or overview page. Most feed types have both a thumbnail and preview visualization, where they are displayed slightly differently. The plugin is not aware of these differences though.
|
||||
|
||||
## PlatformContent
|
||||
All feed objects inherit PlatformContent, and always have the following properties:
|
||||
```kotlin
|
||||
class PlatformContent
|
||||
{
|
||||
id: PlatformID,
|
||||
name: String,
|
||||
thumbnails: ThumbNails,
|
||||
author: PlatformAuthorLink,
|
||||
datetime: Int, // (UnixTimeStamp)
|
||||
url: String
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## PlatformVideo
|
||||
A feed object representing a video or audio.
|
||||
*Usage:*
|
||||
```javascript
|
||||
new PlatformVideo({
|
||||
id: new PlatformID("SomePlatformName", "SomeId", config.id),
|
||||
name: "Some Video Name",
|
||||
thumbnails: new Thumbnails([
|
||||
new Thumbnail("https://.../...", 720),
|
||||
new Thumbnail("https://.../...", 1080),
|
||||
]),
|
||||
author: new AuthorLink(
|
||||
new PlatformID("SomePlatformName", "SomeAuthorID", config.id),
|
||||
"SomeAuthorName",
|
||||
"https://platform.com/your/channel/url",
|
||||
"../url/to/thumbnail.png"),
|
||||
uploadDate: 1696880568,
|
||||
duration: 120,
|
||||
viewCount: 1234567,
|
||||
url: "https://platform.com/your/detail/url",
|
||||
isLive: false
|
||||
});
|
||||
```
|
||||
|
||||
## PlatformPost
|
||||
A feed object representing a community post with text, and optionally images.
|
||||
|
||||
*Usage:*
|
||||
```javascript
|
||||
new PlatformPost{
|
||||
id: new PlatformID(config.name, item?.id, config.id),
|
||||
name: item?.attributes?.title,
|
||||
author: getPlatformAuthorLink(item, context),
|
||||
datetime: (Date.parse(item?.attributes?.published_at) / 1000),
|
||||
url: item?.attributes?.url,
|
||||
description: "Description of Post",
|
||||
images: ["../url/to/image1.png", "../url/to/image2.png"],
|
||||
thumbnails: new Thumbnails([
|
||||
new Thumbnail("https://.../...", 720),
|
||||
new Thumbnail("https://.../...", 1080),
|
||||
])
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
## PlatformNestedMediaContent
|
||||
A feed object representing a link to a different item (often handled by a different plugin).
|
||||
|
||||
An example is a Patreon video, that links to an unlisted Youtube video. If no plugin exists to handle the content, it will be opened in an in-app browser.
|
||||
|
||||
A nested item consists of an detail url and optional metadata such as name, description, thumbnails, etc.
|
||||
*Usage:*
|
||||
```javascript
|
||||
new PlatformNestedMediaContent({
|
||||
id: new PlatformID("SomePlatformName", "SomeId", config.id),
|
||||
name: "Name of content link",
|
||||
author: new AuthorLink(
|
||||
new PlatformID("SomePlatformName", "SomeAuthorID", config.id),
|
||||
"SomeAuthorName",
|
||||
"https://platform.com/your/channel/url",
|
||||
"../url/to/thumbnail.png"),,
|
||||
datetime: 1696880568,
|
||||
url: item?.attributes?.url,
|
||||
contentUrl: "https://someplatform.com/detail/url",
|
||||
contentName: "OptionalName",
|
||||
contentDescription: "OptionalDescription",
|
||||
contentProvider: "OptionalPlatformName",
|
||||
contentThumbnails: new Thumbnails([
|
||||
new Thumbnail("https://.../...", 720),
|
||||
new Thumbnail("https://.../...", 1080),
|
||||
])
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
|
||||
# Detail Types
|
||||
Detail types represent content on a detail page.
|
||||
|
||||
## PlatformVideoDetails
|
||||
|
||||
A detail object representing a video or audio. It inherits PlatformVideo.
|
||||
|
||||
|
||||
### Usage:
|
||||
```javascript
|
||||
new PlatformVideoDetails({
|
||||
id: new PlatformID("SomePlatformName", "SomeId", config.id),
|
||||
name: "Some Video Name",
|
||||
thumbnails: new Thumbnails([
|
||||
new Thumbnail("https://.../...", 720),
|
||||
new Thumbnail("https://.../...", 1080),
|
||||
]),
|
||||
author: new AuthorLink(
|
||||
new PlatformID("SomePlatformName", "SomeAuthorID", config.id),
|
||||
"SomeAuthorName",
|
||||
"https://platform.com/your/channel/url",
|
||||
"../url/to/thumbnail.png"),
|
||||
uploadDate: 1696880568,
|
||||
duration: 120,
|
||||
viewCount: 1234567,
|
||||
url: "https://platform.com/your/detail/url",
|
||||
isLive: false,
|
||||
|
||||
description: "Some description",
|
||||
video: new VideoSourceDescriptor([]), //See sources
|
||||
live: null,
|
||||
rating: new RatingLikes(123),
|
||||
subtitles: []
|
||||
});
|
||||
```
|
||||
### Live Streams
|
||||
If your video is live, the ```isLive``` property should be ```true```, and the ```live``` property should be set to a ```HLSSource```, ```DashSource```, or equivelant.
|
||||
|
||||
### UnMuxed and Audio-Only
|
||||
If your content is either audio-only (eg. music), or has seperate video/audio tracks, you want to use ```UnMuxedVideoDescriptor``` instead of ```VideoSourceDescriptor```:
|
||||
```javascript
|
||||
new UnMuxedVideoDescriptor(
|
||||
[videoSource1, videoSource2, ...],
|
||||
[audioSource1, audioSource2, ...]
|
||||
);
|
||||
```
|
||||
|
||||
### Sources
|
||||
Inside a VideoDescriptor you need to provide an array of sources.
|
||||
Below you can find several source types that Grayjay supports:
|
||||
|
||||
**Standard Url Video/Audio**
|
||||
These are videos available directly on a single url.
|
||||
```javascript
|
||||
new VideoUrlSource({
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
container: "video/mp4",
|
||||
codec: "avc1.4d401e",
|
||||
name: "1080p30 mp4",
|
||||
bitrate: 188103,
|
||||
duration: 250,
|
||||
url: "https://platform.com/some/video/url.mp4"
|
||||
});
|
||||
//For audio:
|
||||
new AudioUrlSource({
|
||||
container: "audio/mp4",
|
||||
codec: "mp4a.40.2",
|
||||
name: "mp4a.40.2",
|
||||
bitrate: 131294,
|
||||
duration: 250,
|
||||
url: "https://platform.com/some/video/url.mp4a",
|
||||
language: "Unknown"
|
||||
});
|
||||
```
|
||||
**Range Url Video/Audio**
|
||||
These are more complex url sources that require very specific range headers to function. They require correct initialization and index positions.
|
||||
These are converted to Dash manifests.
|
||||
|
||||
```javascript
|
||||
new VideoUrlRangeSource({
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
container: "video/mp4",
|
||||
codec: "avc1.4d401e",
|
||||
name: "1080p30 mp4",
|
||||
bitrate: 188103,
|
||||
duration: 250,
|
||||
url: "https://platform.com/some/video/url.mp4",
|
||||
itagId: 1234, //Optional
|
||||
initStart: 0,
|
||||
initEnd: 219,
|
||||
indexStart: 220,
|
||||
indexEnd: 791
|
||||
});
|
||||
//For Audio
|
||||
new AudioUrlRangeSource({
|
||||
container: "audio/mp4",
|
||||
codec: "mp4a.40.2",
|
||||
name: "mp4a.40.2",
|
||||
bitrate: 131294,
|
||||
duration: 250,
|
||||
url: "https://platform.com/some/video/url.mp4a",
|
||||
language: "Unknown"
|
||||
itagId: 1234, //Optional
|
||||
initStart: 0,
|
||||
initEnd: 219,
|
||||
indexStart: 220,
|
||||
indexEnd: 791,
|
||||
audioChannels: 2
|
||||
});
|
||||
```
|
||||
|
||||
**HLSSource**
|
||||
These are sources that are described in a HLS Manifest.
|
||||
```javascript
|
||||
new HLSSource({
|
||||
name: "SomeName", //Optional
|
||||
duration: 250, //Optional
|
||||
url: "https://platform.com/some/hls/manifest.m3u8",
|
||||
priority: false, //Optional
|
||||
language: "Unknown" //Optional
|
||||
});
|
||||
```
|
||||
Generally, HLS sources deprioritized in Grayjay. However if your platform requires HLS sources to be prioritized, you set ```priority``` to ```true```.
|
||||
|
||||
**DashSource**
|
||||
These are sources that are described in a Dash Manifest.
|
||||
```javascript
|
||||
new DashSource({
|
||||
name: "SomeName", //Optional
|
||||
duration: 250, //Optional
|
||||
url: "https://platform.com/some/dash/manifest.mpd"
|
||||
});
|
||||
```
|
||||
|
||||
## PlatformPostDetails
|
||||
A detail object representing a text with optionally accompanying images. The text can be either raw text or html (and possibly in future markup).
|
||||
|
||||
### Usage:
|
||||
```javascript
|
||||
new PlatformPostDetails{
|
||||
id: new PlatformID(config.name, item?.id, config.id),
|
||||
name: item?.attributes?.title,
|
||||
author: getPlatformAuthorLink(item, context),
|
||||
datetime: (Date.parse(item?.attributes?.published_at) / 1000),
|
||||
url: item?.attributes?.url,
|
||||
description: "Description of Post",
|
||||
images: ["../url/to/image1.png", "../url/to/image2.png"],
|
||||
thumbnails: new Thumbnails([
|
||||
new Thumbnail("https://.../thumbnail1.png", 720),
|
||||
new Thumbnail("https://.../thumbnail2.png", 1080),
|
||||
]),
|
||||
rating: new RatingLikes(123),
|
||||
textType: Type.Text.Html/Raw/Markup,
|
||||
content: "Your post content in either raw, html, or in future markup."
|
||||
});
|
||||
```
|
||||
|
||||
|
40
docs/Pagers.md
Normal file
40
docs/Pagers.md
Normal file
|
@ -0,0 +1,40 @@
|
|||
# Pagers
|
||||
Within Grayjay there are several situations where Pagers are used to communicate multiple pages of data back to the app. Some examples are home feed, channel contents, comments, live events, etc.
|
||||
|
||||
All these pagers have exact same layout and usage, with only some very specific cases where additional functionality is exposed.
|
||||
|
||||
Some example of base pagers that exist:
|
||||
|
||||
**ContentPager** for feed objects
|
||||
**ChannelPager** for channels
|
||||
**PlaylistPager** for playlists
|
||||
**CommentPager** for comments
|
||||
|
||||
An example of a pager implementation is as follows:
|
||||
```javascript
|
||||
class MyPlatformContentPager extends ContentPager {
|
||||
constructor(someInfo) {
|
||||
super([], true); //Alternatively, pass first page results in []
|
||||
this.someInfo = someInfo;
|
||||
}
|
||||
|
||||
nextPage() {
|
||||
const myNewResults = //Fetch your next page
|
||||
this.results = myNewResults;
|
||||
this.hasMore = true; //Or false if last page
|
||||
}
|
||||
}
|
||||
```
|
||||
You can also choose to return an entirely new pager object in nextPage, but this is **NOT RECOMMENDED** as it generates a new object for every page. But can be convenient in some recursive situations.
|
||||
```
|
||||
nextPage() {
|
||||
return new MyPlatformContentPager(...);
|
||||
}
|
||||
```
|
||||
In this case the new pager will replace the parent.
|
||||
|
||||
If you ever just want to return an empty pager without any results, you can choose to directly use the base pagers as follows:
|
||||
```
|
||||
return new ContentPager([], false);
|
||||
```
|
||||
Which effectively says *"First page is empty, and no next page"*.
|
Loading…
Add table
Reference in a new issue