mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-27 23:40:33 +05:30
feat: support for local feed extraction
This commit is contained in:
parent
2698368aee
commit
579fae287c
663
app/schemas/com.github.libretube.db.AppDatabase/19.json
Normal file
663
app/schemas/com.github.libretube.db.AppDatabase/19.json
Normal file
@ -0,0 +1,663 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 19,
|
||||||
|
"identityHash": "74afbaf921a81ccd97447002122d5077",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "watchHistoryItem",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `title` TEXT, `uploadDate` TEXT, `uploader` TEXT, `uploaderUrl` TEXT, `uploaderAvatar` TEXT, `thumbnailUrl` TEXT, `duration` INTEGER, `isShort` INTEGER NOT NULL, PRIMARY KEY(`videoId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "videoId",
|
||||||
|
"columnName": "videoId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploadDate",
|
||||||
|
"columnName": "uploadDate",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderUrl",
|
||||||
|
"columnName": "uploaderUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderAvatar",
|
||||||
|
"columnName": "uploaderAvatar",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnailUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isShort",
|
||||||
|
"columnName": "isShort",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"videoId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "watchPosition",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`videoId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "videoId",
|
||||||
|
"columnName": "videoId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "position",
|
||||||
|
"columnName": "position",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"videoId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "searchHistoryItem",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`query` TEXT NOT NULL, PRIMARY KEY(`query`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "query",
|
||||||
|
"columnName": "query",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"query"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "customInstance",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `apiUrl` TEXT NOT NULL, `frontendUrl` TEXT NOT NULL, PRIMARY KEY(`name`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "apiUrl",
|
||||||
|
"columnName": "apiUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "frontendUrl",
|
||||||
|
"columnName": "frontendUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "localSubscription",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`channelId` TEXT NOT NULL, PRIMARY KEY(`channelId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "channelId",
|
||||||
|
"columnName": "channelId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"channelId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlistBookmark",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlistId` TEXT NOT NULL, `playlistName` TEXT, `thumbnailUrl` TEXT, `uploader` TEXT, `uploaderUrl` TEXT, `uploaderAvatar` TEXT, `videos` INTEGER NOT NULL, PRIMARY KEY(`playlistId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistId",
|
||||||
|
"columnName": "playlistId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistName",
|
||||||
|
"columnName": "playlistName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnailUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderUrl",
|
||||||
|
"columnName": "uploaderUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderAvatar",
|
||||||
|
"columnName": "uploaderAvatar",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videos",
|
||||||
|
"columnName": "videos",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"playlistId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "LocalPlaylist",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT NOT NULL, `description` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnailUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "LocalPlaylistItem",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` INTEGER NOT NULL, `videoId` TEXT NOT NULL, `title` TEXT, `uploadDate` TEXT, `uploader` TEXT, `uploaderUrl` TEXT, `uploaderAvatar` TEXT, `thumbnailUrl` TEXT, `duration` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistId",
|
||||||
|
"columnName": "playlistId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoId",
|
||||||
|
"columnName": "videoId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploadDate",
|
||||||
|
"columnName": "uploadDate",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderUrl",
|
||||||
|
"columnName": "uploaderUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderAvatar",
|
||||||
|
"columnName": "uploaderAvatar",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnailUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "download",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `title` TEXT NOT NULL, `description` TEXT NOT NULL, `uploader` TEXT NOT NULL, `duration` INTEGER DEFAULT NULL, `uploadDate` TEXT, `thumbnailPath` TEXT, PRIMARY KEY(`videoId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "videoId",
|
||||||
|
"columnName": "videoId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false,
|
||||||
|
"defaultValue": "NULL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploadDate",
|
||||||
|
"columnName": "uploadDate",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailPath",
|
||||||
|
"columnName": "thumbnailPath",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"videoId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "downloadItem",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT NOT NULL, `videoId` TEXT NOT NULL, `fileName` TEXT NOT NULL, `path` TEXT NOT NULL, `url` TEXT, `format` TEXT, `quality` TEXT, `language` TEXT, `downloadSize` INTEGER NOT NULL, FOREIGN KEY(`videoId`) REFERENCES `download`(`videoId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "type",
|
||||||
|
"columnName": "type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoId",
|
||||||
|
"columnName": "videoId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "fileName",
|
||||||
|
"columnName": "fileName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "path",
|
||||||
|
"columnName": "path",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "format",
|
||||||
|
"columnName": "format",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "quality",
|
||||||
|
"columnName": "quality",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "language",
|
||||||
|
"columnName": "language",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "downloadSize",
|
||||||
|
"columnName": "downloadSize",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_downloadItem_path",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"path"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_downloadItem_path` ON `${TABLE_NAME}` (`path`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "download",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"videoId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"videoId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "downloadChapters",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `videoId` TEXT NOT NULL, `name` TEXT NOT NULL, `start` INTEGER NOT NULL, `thumbnailUrl` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoId",
|
||||||
|
"columnName": "videoId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "start",
|
||||||
|
"columnName": "start",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnailUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "subscriptionGroups",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `channels` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`name`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "channels",
|
||||||
|
"columnName": "channels",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feedItem",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `title` TEXT, `thumbnail` TEXT, `uploaderName` TEXT, `uploaderUrl` TEXT, `uploaderAvatar` TEXT, `duration` INTEGER, `views` INTEGER, `uploaderVerified` INTEGER NOT NULL, `uploaded` INTEGER NOT NULL, `shortDescription` TEXT, `isShort` INTEGER NOT NULL, PRIMARY KEY(`videoId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "videoId",
|
||||||
|
"columnName": "videoId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnail",
|
||||||
|
"columnName": "thumbnail",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderName",
|
||||||
|
"columnName": "uploaderName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderUrl",
|
||||||
|
"columnName": "uploaderUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderAvatar",
|
||||||
|
"columnName": "uploaderAvatar",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "views",
|
||||||
|
"columnName": "views",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderVerified",
|
||||||
|
"columnName": "uploaderVerified",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaded",
|
||||||
|
"columnName": "uploaded",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "shortDescription",
|
||||||
|
"columnName": "shortDescription",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isShort",
|
||||||
|
"columnName": "isShort",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"videoId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '74afbaf921a81ccd97447002122d5077')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ import androidx.core.app.NotificationChannelCompat
|
|||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
import com.github.libretube.helpers.ImageHelper
|
import com.github.libretube.helpers.ImageHelper
|
||||||
|
import com.github.libretube.helpers.NewPipeExtractorInstance
|
||||||
import com.github.libretube.helpers.NotificationHelper
|
import com.github.libretube.helpers.NotificationHelper
|
||||||
import com.github.libretube.helpers.PreferenceHelper
|
import com.github.libretube.helpers.PreferenceHelper
|
||||||
import com.github.libretube.helpers.ProxyHelper
|
import com.github.libretube.helpers.ProxyHelper
|
||||||
@ -55,6 +56,8 @@ class LibreTubeApp : Application() {
|
|||||||
* Dynamically create App Shortcuts
|
* Dynamically create App Shortcuts
|
||||||
*/
|
*/
|
||||||
ShortcutHelper.createShortcuts(this)
|
ShortcutHelper.createShortcuts(this)
|
||||||
|
|
||||||
|
NewPipeExtractorInstance.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -11,15 +11,13 @@ import com.github.libretube.api.obj.StreamItem
|
|||||||
import com.github.libretube.api.obj.Streams
|
import com.github.libretube.api.obj.Streams
|
||||||
import com.github.libretube.api.obj.Subtitle
|
import com.github.libretube.api.obj.Subtitle
|
||||||
import com.github.libretube.helpers.PlayerHelper
|
import com.github.libretube.helpers.PlayerHelper
|
||||||
import com.github.libretube.util.NewPipeDownloaderImpl
|
import com.github.libretube.ui.dialogs.ShareDialog.Companion.YOUTUBE_FRONTEND_URL
|
||||||
import kotlinx.datetime.toKotlinInstant
|
import kotlinx.datetime.toKotlinInstant
|
||||||
import org.schabi.newpipe.extractor.NewPipe
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo
|
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream
|
import org.schabi.newpipe.extractor.stream.VideoStream
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.lang.Exception
|
|
||||||
|
|
||||||
fun VideoStream.toPipedStream(): PipedStream = PipedStream(
|
fun VideoStream.toPipedStream(): PipedStream = PipedStream(
|
||||||
url = content,
|
url = content,
|
||||||
@ -39,26 +37,18 @@ fun VideoStream.toPipedStream(): PipedStream = PipedStream(
|
|||||||
)
|
)
|
||||||
|
|
||||||
object StreamsExtractor {
|
object StreamsExtractor {
|
||||||
// val npe by lazy {
|
|
||||||
// NewPipe.getService(ServiceList.YouTube.serviceId)
|
|
||||||
// }
|
|
||||||
|
|
||||||
init {
|
|
||||||
NewPipe.init(NewPipeDownloaderImpl())
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun extractStreams(videoId: String): Streams {
|
suspend fun extractStreams(videoId: String): Streams {
|
||||||
if (!PlayerHelper.disablePipedProxy || !PlayerHelper.localStreamExtraction) {
|
if (!PlayerHelper.disablePipedProxy || !PlayerHelper.localStreamExtraction) {
|
||||||
return RetrofitInstance.api.getStreams(videoId)
|
return RetrofitInstance.api.getStreams(videoId)
|
||||||
}
|
}
|
||||||
|
|
||||||
val resp = StreamInfo.getInfo("https://www.youtube.com/watch?v=$videoId")
|
val resp = StreamInfo.getInfo("${YOUTUBE_FRONTEND_URL}/watch?v=$videoId")
|
||||||
return Streams(
|
return Streams(
|
||||||
title = resp.name,
|
title = resp.name,
|
||||||
description = resp.description.content,
|
description = resp.description.content,
|
||||||
uploader = resp.uploaderName,
|
uploader = resp.uploaderName,
|
||||||
uploaderAvatar = resp.uploaderAvatars.maxBy { it.height }.url,
|
uploaderAvatar = resp.uploaderAvatars.maxBy { it.height }.url,
|
||||||
uploaderUrl = resp.uploaderUrl.replace("https://www.youtube.com", ""),
|
uploaderUrl = resp.uploaderUrl.replace(YOUTUBE_FRONTEND_URL, ""),
|
||||||
uploaderVerified = resp.isUploaderVerified,
|
uploaderVerified = resp.isUploaderVerified,
|
||||||
uploaderSubscriberCount = resp.uploaderSubscriberCount,
|
uploaderSubscriberCount = resp.uploaderSubscriberCount,
|
||||||
category = resp.category,
|
category = resp.category,
|
||||||
@ -86,12 +76,12 @@ object StreamsExtractor {
|
|||||||
thumbnailUrl = resp.thumbnails.maxBy { it.height }.url,
|
thumbnailUrl = resp.thumbnails.maxBy { it.height }.url,
|
||||||
relatedStreams = resp.relatedItems.filterIsInstance<StreamInfoItem>().map {
|
relatedStreams = resp.relatedItems.filterIsInstance<StreamInfoItem>().map {
|
||||||
StreamItem(
|
StreamItem(
|
||||||
it.url.replace("https://www.youtube.com", ""),
|
it.url.replace(YOUTUBE_FRONTEND_URL, ""),
|
||||||
StreamItem.TYPE_STREAM,
|
StreamItem.TYPE_STREAM,
|
||||||
it.name,
|
it.name,
|
||||||
it.thumbnails.maxBy { image -> image.height }.url,
|
it.thumbnails.maxBy { image -> image.height }.url,
|
||||||
it.uploaderName,
|
it.uploaderName,
|
||||||
it.uploaderUrl.replace("https://www.youtube.com", ""),
|
it.uploaderUrl.replace(YOUTUBE_FRONTEND_URL, ""),
|
||||||
it.uploaderAvatars.maxBy { image -> image.height }.url,
|
it.uploaderAvatars.maxBy { image -> image.height }.url,
|
||||||
it.textualUploadDate,
|
it.textualUploadDate,
|
||||||
it.duration,
|
it.duration,
|
||||||
|
@ -6,6 +6,7 @@ import com.github.libretube.constants.PreferenceKeys
|
|||||||
import com.github.libretube.helpers.PreferenceHelper
|
import com.github.libretube.helpers.PreferenceHelper
|
||||||
import com.github.libretube.repo.AccountSubscriptionsRepository
|
import com.github.libretube.repo.AccountSubscriptionsRepository
|
||||||
import com.github.libretube.repo.FeedRepository
|
import com.github.libretube.repo.FeedRepository
|
||||||
|
import com.github.libretube.repo.LocalFeedRepository
|
||||||
import com.github.libretube.repo.LocalSubscriptionsRepository
|
import com.github.libretube.repo.LocalSubscriptionsRepository
|
||||||
import com.github.libretube.repo.PipedAccountFeedRepository
|
import com.github.libretube.repo.PipedAccountFeedRepository
|
||||||
import com.github.libretube.repo.PipedNoAccountFeedRepository
|
import com.github.libretube.repo.PipedNoAccountFeedRepository
|
||||||
@ -26,6 +27,7 @@ object SubscriptionHelper {
|
|||||||
else -> LocalSubscriptionsRepository()
|
else -> LocalSubscriptionsRepository()
|
||||||
}
|
}
|
||||||
private val feedRepository: FeedRepository get() = when {
|
private val feedRepository: FeedRepository get() = when {
|
||||||
|
PreferenceHelper.getBoolean(PreferenceKeys.LOCAL_FEED_EXTRACTION, false) -> LocalFeedRepository()
|
||||||
token.isNotEmpty() -> PipedAccountFeedRepository()
|
token.isNotEmpty() -> PipedAccountFeedRepository()
|
||||||
else -> PipedNoAccountFeedRepository()
|
else -> PipedNoAccountFeedRepository()
|
||||||
}
|
}
|
||||||
@ -35,7 +37,8 @@ object SubscriptionHelper {
|
|||||||
suspend fun isSubscribed(channelId: String) = subscriptionsRepository.isSubscribed(channelId)
|
suspend fun isSubscribed(channelId: String) = subscriptionsRepository.isSubscribed(channelId)
|
||||||
suspend fun importSubscriptions(newChannels: List<String>) = subscriptionsRepository.importSubscriptions(newChannels)
|
suspend fun importSubscriptions(newChannels: List<String>) = subscriptionsRepository.importSubscriptions(newChannels)
|
||||||
suspend fun getSubscriptions() = subscriptionsRepository.getSubscriptions()
|
suspend fun getSubscriptions() = subscriptionsRepository.getSubscriptions()
|
||||||
suspend fun getFeed() = feedRepository.getFeed(false)
|
suspend fun getSubscriptionChannelIds() = subscriptionsRepository.getSubscriptionChannelIds()
|
||||||
|
suspend fun getFeed(forceRefresh: Boolean) = feedRepository.getFeed(forceRefresh)
|
||||||
|
|
||||||
fun handleUnsubscribe(
|
fun handleUnsubscribe(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
@ -2,6 +2,7 @@ package com.github.libretube.api.obj
|
|||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.github.libretube.db.obj.LocalPlaylistItem
|
import com.github.libretube.db.obj.LocalPlaylistItem
|
||||||
|
import com.github.libretube.db.obj.SubscriptionsFeedItem
|
||||||
import com.github.libretube.extensions.toID
|
import com.github.libretube.extensions.toID
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
@ -40,6 +41,21 @@ data class StreamItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun toFeedItem() = SubscriptionsFeedItem(
|
||||||
|
videoId = url!!.toID(),
|
||||||
|
title = title,
|
||||||
|
thumbnail = thumbnail,
|
||||||
|
uploaderName = uploaderName,
|
||||||
|
uploaded = uploaded,
|
||||||
|
uploaderAvatar = uploaderAvatar,
|
||||||
|
uploaderUrl = uploaderUrl,
|
||||||
|
duration = duration,
|
||||||
|
uploaderVerified = uploaderVerified ?: false,
|
||||||
|
shortDescription = shortDescription,
|
||||||
|
views = views,
|
||||||
|
isShort = isShort
|
||||||
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TYPE_STREAM = "stream"
|
const val TYPE_STREAM = "stream"
|
||||||
const val TYPE_CHANNEL = "channel"
|
const val TYPE_CHANNEL = "channel"
|
||||||
|
@ -3,12 +3,11 @@ package com.github.libretube.api.obj
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.github.libretube.db.obj.DownloadItem
|
import com.github.libretube.db.obj.DownloadItem
|
||||||
import com.github.libretube.enums.FileType
|
import com.github.libretube.enums.FileType
|
||||||
|
import com.github.libretube.extensions.toLocalDate
|
||||||
import com.github.libretube.helpers.ProxyHelper
|
import com.github.libretube.helpers.ProxyHelper
|
||||||
import com.github.libretube.json.SafeInstantSerializer
|
import com.github.libretube.json.SafeInstantSerializer
|
||||||
import com.github.libretube.parcelable.DownloadData
|
import com.github.libretube.parcelable.DownloadData
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.datetime.TimeZone
|
|
||||||
import kotlinx.datetime.toLocalDateTime
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
@ -100,8 +99,7 @@ data class Streams(
|
|||||||
uploaderName = uploader,
|
uploaderName = uploader,
|
||||||
uploaderUrl = uploaderUrl,
|
uploaderUrl = uploaderUrl,
|
||||||
uploaderAvatar = uploaderAvatar,
|
uploaderAvatar = uploaderAvatar,
|
||||||
uploadedDate = uploadTimestamp?.toLocalDateTime(TimeZone.currentSystemDefault())?.date
|
uploadedDate = uploadTimestamp?.toLocalDate()?.toString(),
|
||||||
?.toString(),
|
|
||||||
uploaded = uploaded ?: uploadTimestamp?.toEpochMilliseconds() ?: 0,
|
uploaded = uploaded ?: uploadTimestamp?.toEpochMilliseconds() ?: 0,
|
||||||
duration = duration,
|
duration = duration,
|
||||||
views = views,
|
views = views,
|
||||||
@ -111,6 +109,6 @@ data class Streams(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val categoryMusic = "Music"
|
const val CATEGORY_MUSIC = "Music"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -113,6 +113,8 @@ object PreferenceKeys {
|
|||||||
const val HIDE_WATCHED_FROM_FEED = "hide_watched_from_feed"
|
const val HIDE_WATCHED_FROM_FEED = "hide_watched_from_feed"
|
||||||
const val SELECTED_FEED_FILTERS = "filter_feed"
|
const val SELECTED_FEED_FILTERS = "filter_feed"
|
||||||
const val FEED_SORT_ORDER = "sort_oder_feed"
|
const val FEED_SORT_ORDER = "sort_oder_feed"
|
||||||
|
const val LOCAL_FEED_EXTRACTION = "local_feed_extraction"
|
||||||
|
const val LAST_FEED_REFRESH_TIMESTAMP_MILLIS = "last_feed_refresh_timestamp_millis"
|
||||||
|
|
||||||
// Advanced
|
// Advanced
|
||||||
const val AUTOMATIC_UPDATE_CHECKS = "automatic_update_checks"
|
const val AUTOMATIC_UPDATE_CHECKS = "automatic_update_checks"
|
||||||
|
@ -11,6 +11,7 @@ import com.github.libretube.db.dao.LocalSubscriptionDao
|
|||||||
import com.github.libretube.db.dao.PlaylistBookmarkDao
|
import com.github.libretube.db.dao.PlaylistBookmarkDao
|
||||||
import com.github.libretube.db.dao.SearchHistoryDao
|
import com.github.libretube.db.dao.SearchHistoryDao
|
||||||
import com.github.libretube.db.dao.SubscriptionGroupsDao
|
import com.github.libretube.db.dao.SubscriptionGroupsDao
|
||||||
|
import com.github.libretube.db.dao.SubscriptionsFeedDao
|
||||||
import com.github.libretube.db.dao.WatchHistoryDao
|
import com.github.libretube.db.dao.WatchHistoryDao
|
||||||
import com.github.libretube.db.dao.WatchPositionDao
|
import com.github.libretube.db.dao.WatchPositionDao
|
||||||
import com.github.libretube.db.obj.CustomInstance
|
import com.github.libretube.db.obj.CustomInstance
|
||||||
@ -23,6 +24,7 @@ import com.github.libretube.db.obj.LocalSubscription
|
|||||||
import com.github.libretube.db.obj.PlaylistBookmark
|
import com.github.libretube.db.obj.PlaylistBookmark
|
||||||
import com.github.libretube.db.obj.SearchHistoryItem
|
import com.github.libretube.db.obj.SearchHistoryItem
|
||||||
import com.github.libretube.db.obj.SubscriptionGroup
|
import com.github.libretube.db.obj.SubscriptionGroup
|
||||||
|
import com.github.libretube.db.obj.SubscriptionsFeedItem
|
||||||
import com.github.libretube.db.obj.WatchHistoryItem
|
import com.github.libretube.db.obj.WatchHistoryItem
|
||||||
import com.github.libretube.db.obj.WatchPosition
|
import com.github.libretube.db.obj.WatchPosition
|
||||||
|
|
||||||
@ -39,15 +41,17 @@ import com.github.libretube.db.obj.WatchPosition
|
|||||||
Download::class,
|
Download::class,
|
||||||
DownloadItem::class,
|
DownloadItem::class,
|
||||||
DownloadChapter::class,
|
DownloadChapter::class,
|
||||||
SubscriptionGroup::class
|
SubscriptionGroup::class,
|
||||||
|
SubscriptionsFeedItem::class
|
||||||
],
|
],
|
||||||
version = 18,
|
version = 19,
|
||||||
autoMigrations = [
|
autoMigrations = [
|
||||||
AutoMigration(from = 7, to = 8),
|
AutoMigration(from = 7, to = 8),
|
||||||
AutoMigration(from = 8, to = 9),
|
AutoMigration(from = 8, to = 9),
|
||||||
AutoMigration(from = 9, to = 10),
|
AutoMigration(from = 9, to = 10),
|
||||||
AutoMigration(from = 10, to = 11),
|
AutoMigration(from = 10, to = 11),
|
||||||
AutoMigration(from = 16, to = 17)
|
AutoMigration(from = 16, to = 17),
|
||||||
|
AutoMigration(from = 18, to = 19)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
@ -96,4 +100,6 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
* Subscription groups
|
* Subscription groups
|
||||||
*/
|
*/
|
||||||
abstract fun subscriptionGroupsDao(): SubscriptionGroupsDao
|
abstract fun subscriptionGroupsDao(): SubscriptionGroupsDao
|
||||||
|
|
||||||
|
abstract fun feedDao(): SubscriptionsFeedDao
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ package com.github.libretube.db
|
|||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import com.github.libretube.api.JsonHelper
|
import com.github.libretube.api.JsonHelper
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import kotlinx.datetime.toLocalDate
|
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import kotlin.io.path.Path
|
import kotlin.io.path.Path
|
||||||
@ -13,7 +12,7 @@ object Converters {
|
|||||||
fun localDateToString(localDate: LocalDate?) = localDate?.toString()
|
fun localDateToString(localDate: LocalDate?) = localDate?.toString()
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun stringToLocalDate(string: String?) = string?.toLocalDate()
|
fun stringToLocalDate(string: String?) = string?.let { LocalDate.parse(it) }
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun pathToString(path: Path?) = path?.toString()
|
fun pathToString(path: Path?) = path?.toString()
|
||||||
|
@ -8,13 +8,11 @@ import com.github.libretube.db.obj.SearchHistoryItem
|
|||||||
import com.github.libretube.db.obj.WatchHistoryItem
|
import com.github.libretube.db.obj.WatchHistoryItem
|
||||||
import com.github.libretube.enums.ContentFilter
|
import com.github.libretube.enums.ContentFilter
|
||||||
import com.github.libretube.extensions.toID
|
import com.github.libretube.extensions.toID
|
||||||
|
import com.github.libretube.extensions.toLocalDate
|
||||||
import com.github.libretube.helpers.PreferenceHelper
|
import com.github.libretube.helpers.PreferenceHelper
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.datetime.Instant
|
|
||||||
import kotlinx.datetime.TimeZone
|
|
||||||
import kotlinx.datetime.toLocalDateTime
|
|
||||||
|
|
||||||
object DatabaseHelper {
|
object DatabaseHelper {
|
||||||
private const val MAX_SEARCH_HISTORY_SIZE = 20
|
private const val MAX_SEARCH_HISTORY_SIZE = 20
|
||||||
@ -28,8 +26,7 @@ object DatabaseHelper {
|
|||||||
val watchHistoryItem = WatchHistoryItem(
|
val watchHistoryItem = WatchHistoryItem(
|
||||||
videoId,
|
videoId,
|
||||||
stream.title,
|
stream.title,
|
||||||
Instant.fromEpochMilliseconds(stream.uploaded)
|
stream.uploaded.toLocalDate(),
|
||||||
.toLocalDateTime(TimeZone.currentSystemDefault()).date,
|
|
||||||
stream.uploaderName,
|
stream.uploaderName,
|
||||||
stream.uploaderUrl?.toID(),
|
stream.uploaderUrl?.toID(),
|
||||||
stream.uploaderAvatar,
|
stream.uploaderAvatar,
|
||||||
|
@ -66,7 +66,6 @@ object DatabaseHolder {
|
|||||||
MIGRATION_15_16,
|
MIGRATION_15_16,
|
||||||
MIGRATION_17_18
|
MIGRATION_17_18
|
||||||
)
|
)
|
||||||
.fallbackToDestructiveMigration()
|
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
package com.github.libretube.db.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import com.github.libretube.db.obj.SubscriptionsFeedItem
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface SubscriptionsFeedDao {
|
||||||
|
@Query("SELECT * FROM feedItem ORDER BY uploaded DESC")
|
||||||
|
suspend fun getAll(): List<SubscriptionsFeedItem>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insertAll(feedItems: List<SubscriptionsFeedItem>)
|
||||||
|
|
||||||
|
@Query("DELETE FROM feedItem WHERE uploaded < :olderThan")
|
||||||
|
suspend fun cleanUpOlderThan(olderThan: Long)
|
||||||
|
|
||||||
|
@Query("DELETE FROM feedItem")
|
||||||
|
suspend fun deleteAll()
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
package com.github.libretube.db.obj
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import com.github.libretube.api.obj.StreamItem
|
||||||
|
import com.github.libretube.extensions.toLocalDate
|
||||||
|
|
||||||
|
@Entity(tableName = "feedItem")
|
||||||
|
data class SubscriptionsFeedItem(
|
||||||
|
@PrimaryKey
|
||||||
|
val videoId: String,
|
||||||
|
val title: String? = null,
|
||||||
|
val thumbnail: String? = null,
|
||||||
|
val uploaderName: String? = null,
|
||||||
|
val uploaderUrl: String? = null,
|
||||||
|
val uploaderAvatar: String? = null,
|
||||||
|
val duration: Long? = null,
|
||||||
|
val views: Long? = null,
|
||||||
|
val uploaderVerified: Boolean,
|
||||||
|
val uploaded: Long = 0,
|
||||||
|
val shortDescription: String? = null,
|
||||||
|
val isShort: Boolean = false
|
||||||
|
) {
|
||||||
|
fun toStreamItem() = StreamItem(
|
||||||
|
url = videoId,
|
||||||
|
type = StreamItem.TYPE_STREAM,
|
||||||
|
title = title,
|
||||||
|
thumbnail = thumbnail,
|
||||||
|
uploaderName = uploaderName,
|
||||||
|
uploaded = uploaded,
|
||||||
|
uploadedDate = uploaded.toLocalDate().toString(),
|
||||||
|
uploaderAvatar = uploaderAvatar,
|
||||||
|
uploaderUrl = uploaderUrl,
|
||||||
|
duration = duration,
|
||||||
|
uploaderVerified = uploaderVerified,
|
||||||
|
shortDescription = shortDescription,
|
||||||
|
views = views,
|
||||||
|
isShort = isShort
|
||||||
|
)
|
||||||
|
}
|
@ -30,6 +30,7 @@ data class WatchHistoryItem(
|
|||||||
thumbnail = thumbnailUrl,
|
thumbnail = thumbnailUrl,
|
||||||
uploaderName = uploader,
|
uploaderName = uploader,
|
||||||
uploaded = uploadDate?.toMillis() ?: 0,
|
uploaded = uploadDate?.toMillis() ?: 0,
|
||||||
|
uploadedDate = uploadDate?.toString(),
|
||||||
uploaderAvatar = uploaderAvatar,
|
uploaderAvatar = uploaderAvatar,
|
||||||
uploaderUrl = uploaderUrl,
|
uploaderUrl = uploaderUrl,
|
||||||
duration = duration,
|
duration = duration,
|
||||||
|
@ -1,7 +1,19 @@
|
|||||||
package com.github.libretube.extensions
|
package com.github.libretube.extensions
|
||||||
|
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import kotlinx.datetime.atStartOfDayIn
|
import kotlinx.datetime.atStartOfDayIn
|
||||||
|
import kotlinx.datetime.toLocalDateTime
|
||||||
|
|
||||||
fun LocalDate.toMillis() = this.atStartOfDayIn(TimeZone.UTC).toEpochMilliseconds()
|
fun LocalDate.toMillis() = this.atStartOfDayIn(TimeZone.UTC).toEpochMilliseconds()
|
||||||
|
|
||||||
|
fun Long.toLocalDateTime() =
|
||||||
|
Instant.fromEpochMilliseconds(this).toLocalDateTime()
|
||||||
|
|
||||||
|
fun Long.toLocalDate() =
|
||||||
|
Instant.fromEpochMilliseconds(this).toLocalDate()
|
||||||
|
|
||||||
|
fun Instant.toLocalDateTime() = this.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||||
|
|
||||||
|
fun Instant.toLocalDate() = this.toLocalDateTime().date
|
@ -0,0 +1,16 @@
|
|||||||
|
package com.github.libretube.helpers
|
||||||
|
|
||||||
|
import com.github.libretube.util.NewPipeDownloaderImpl
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe
|
||||||
|
import org.schabi.newpipe.extractor.ServiceList
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService
|
||||||
|
|
||||||
|
object NewPipeExtractorInstance {
|
||||||
|
val extractor: StreamingService by lazy {
|
||||||
|
NewPipe.getService(ServiceList.YouTube.serviceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun init() {
|
||||||
|
NewPipe.init(NewPipeDownloaderImpl())
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ package com.github.libretube.json
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.github.libretube.extensions.TAG
|
import com.github.libretube.extensions.TAG
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
|
import kotlinx.datetime.LocalDate
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import kotlinx.datetime.atStartOfDayIn
|
import kotlinx.datetime.atStartOfDayIn
|
||||||
import kotlinx.datetime.toInstant
|
import kotlinx.datetime.toInstant
|
||||||
@ -19,10 +20,10 @@ object SafeInstantSerializer : KSerializer<Instant> {
|
|||||||
override fun deserialize(decoder: Decoder): Instant {
|
override fun deserialize(decoder: Decoder): Instant {
|
||||||
val string = decoder.decodeString()
|
val string = decoder.decodeString()
|
||||||
return try {
|
return try {
|
||||||
string.toInstant()
|
Instant.parse(string)
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
Log.e(TAG(), "Error parsing date '$string'", e)
|
Log.e(TAG(), "Error parsing date '$string'", e)
|
||||||
string.toLocalDate().atStartOfDayIn(TimeZone.currentSystemDefault())
|
LocalDate.parse(string).atStartOfDayIn(TimeZone.currentSystemDefault())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ package com.github.libretube.repo
|
|||||||
import com.github.libretube.api.RetrofitInstance
|
import com.github.libretube.api.RetrofitInstance
|
||||||
import com.github.libretube.api.obj.Subscribe
|
import com.github.libretube.api.obj.Subscribe
|
||||||
import com.github.libretube.api.obj.Subscription
|
import com.github.libretube.api.obj.Subscription
|
||||||
|
import com.github.libretube.extensions.toID
|
||||||
import com.github.libretube.helpers.PreferenceHelper
|
import com.github.libretube.helpers.PreferenceHelper
|
||||||
|
|
||||||
class AccountSubscriptionsRepository: SubscriptionsRepository {
|
class AccountSubscriptionsRepository: SubscriptionsRepository {
|
||||||
@ -33,4 +34,8 @@ class AccountSubscriptionsRepository: SubscriptionsRepository {
|
|||||||
override suspend fun getSubscriptions(): List<Subscription> {
|
override suspend fun getSubscriptions(): List<Subscription> {
|
||||||
return RetrofitInstance.authApi.subscriptions(token)
|
return RetrofitInstance.authApi.subscriptions(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getSubscriptionChannelIds(): List<String> {
|
||||||
|
return getSubscriptions().map { it.url.toID() }
|
||||||
|
}
|
||||||
}
|
}
|
@ -0,0 +1,103 @@
|
|||||||
|
package com.github.libretube.repo
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.github.libretube.api.SubscriptionHelper
|
||||||
|
import com.github.libretube.api.obj.StreamItem
|
||||||
|
import com.github.libretube.constants.PreferenceKeys
|
||||||
|
import com.github.libretube.db.DatabaseHolder
|
||||||
|
import com.github.libretube.db.obj.SubscriptionsFeedItem
|
||||||
|
import com.github.libretube.extensions.parallelMap
|
||||||
|
import com.github.libretube.helpers.NewPipeExtractorInstance
|
||||||
|
import com.github.libretube.helpers.PreferenceHelper
|
||||||
|
import com.github.libretube.ui.dialogs.ShareDialog.Companion.YOUTUBE_FRONTEND_URL
|
||||||
|
import org.schabi.newpipe.extractor.channel.ChannelInfo
|
||||||
|
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo
|
||||||
|
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabs
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
class LocalFeedRepository : FeedRepository {
|
||||||
|
private val relevantTabs =
|
||||||
|
arrayOf(ChannelTabs.LIVESTREAMS, ChannelTabs.VIDEOS, ChannelTabs.SHORTS)
|
||||||
|
|
||||||
|
override suspend fun getFeed(forceRefresh: Boolean): List<StreamItem> {
|
||||||
|
val nowMillis = Instant.now().toEpochMilli()
|
||||||
|
|
||||||
|
if (!forceRefresh) {
|
||||||
|
val feed = DatabaseHolder.Database.feedDao().getAll()
|
||||||
|
val oneDayAgo = nowMillis - Duration.ofDays(1).toMillis()
|
||||||
|
|
||||||
|
// only refresh if feed is empty or last refresh was more than a day ago
|
||||||
|
val lastRefresh =
|
||||||
|
PreferenceHelper.getLong(PreferenceKeys.LAST_FEED_REFRESH_TIMESTAMP_MILLIS, 0)
|
||||||
|
if (feed.isNotEmpty() && lastRefresh > oneDayAgo) {
|
||||||
|
return DatabaseHolder.Database.feedDao().getAll()
|
||||||
|
.map(SubscriptionsFeedItem::toStreamItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val minimumDateMillis = nowMillis - Duration.ofDays(MAX_FEED_AGE_DAYS).toMillis()
|
||||||
|
DatabaseHolder.Database.feedDao().cleanUpOlderThan(minimumDateMillis)
|
||||||
|
|
||||||
|
refreshFeed(minimumDateMillis)
|
||||||
|
PreferenceHelper.putLong(PreferenceKeys.LAST_FEED_REFRESH_TIMESTAMP_MILLIS, nowMillis)
|
||||||
|
|
||||||
|
return DatabaseHolder.Database.feedDao().getAll().map(SubscriptionsFeedItem::toStreamItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun refreshFeed(minimumDateMillis: Long) {
|
||||||
|
val channelIds = SubscriptionHelper.getSubscriptionChannelIds()
|
||||||
|
|
||||||
|
for (channelIdChunk in channelIds.chunked(CHUNK_SIZE)) {
|
||||||
|
val collectedFeedItems = channelIdChunk.parallelMap { channelId ->
|
||||||
|
try {
|
||||||
|
getRelatedStreams(channelId)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(channelId, e.stackTraceToString())
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.filterNotNull().flatten().map(StreamItem::toFeedItem)
|
||||||
|
.filter { it.uploaded > minimumDateMillis }
|
||||||
|
|
||||||
|
DatabaseHolder.Database.feedDao().insertAll(collectedFeedItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getRelatedStreams(channelId: String): List<StreamItem> {
|
||||||
|
val channelInfo = ChannelInfo.getInfo("$YOUTUBE_FRONTEND_URL/channel/${channelId}")
|
||||||
|
val relevantInfoTabs = channelInfo.tabs.filter { tab ->
|
||||||
|
relevantTabs.any { tab.contentFilters.contains(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val related = relevantInfoTabs.parallelMap { tab ->
|
||||||
|
runCatching {
|
||||||
|
ChannelTabInfo.getInfo(NewPipeExtractorInstance.extractor, tab).relatedItems
|
||||||
|
}.getOrElse { emptyList() }
|
||||||
|
}.flatten().filterIsInstance<StreamInfoItem>()
|
||||||
|
|
||||||
|
return related.map { item ->
|
||||||
|
StreamItem(
|
||||||
|
type = StreamItem.TYPE_STREAM,
|
||||||
|
url = item.url.replace(YOUTUBE_FRONTEND_URL, ""),
|
||||||
|
title = item.name,
|
||||||
|
uploaded = item.uploadDate?.offsetDateTime()?.toEpochSecond()?.times(1000) ?: 0,
|
||||||
|
uploadedDate = item.uploadDate?.offsetDateTime()?.toLocalDateTime()?.toLocalDate()
|
||||||
|
?.toString(),
|
||||||
|
uploaderName = item.uploaderName,
|
||||||
|
uploaderUrl = item.uploaderUrl.replace(YOUTUBE_FRONTEND_URL, ""),
|
||||||
|
uploaderAvatar = channelInfo.avatars.maxByOrNull { it.height }?.url,
|
||||||
|
thumbnail = item.thumbnails.maxByOrNull { it.height }?.url,
|
||||||
|
duration = item.duration,
|
||||||
|
views = item.viewCount,
|
||||||
|
uploaderVerified = item.isUploaderVerified,
|
||||||
|
shortDescription = item.shortDescription
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val CHUNK_SIZE = 2
|
||||||
|
private const val MAX_FEED_AGE_DAYS = 30L // 30 days
|
||||||
|
}
|
||||||
|
}
|
@ -24,16 +24,20 @@ class LocalSubscriptionsRepository: SubscriptionsRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getSubscriptions(): List<Subscription> {
|
override suspend fun getSubscriptions(): List<Subscription> {
|
||||||
val subscriptions = Database.localSubscriptionDao().getAll().map { it.channelId }
|
val channelIds = getSubscriptionChannelIds()
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
subscriptions.size > GET_SUBSCRIPTIONS_LIMIT ->
|
channelIds.size > GET_SUBSCRIPTIONS_LIMIT ->
|
||||||
RetrofitInstance.authApi
|
RetrofitInstance.authApi
|
||||||
.unauthenticatedSubscriptions(subscriptions)
|
.unauthenticatedSubscriptions(channelIds)
|
||||||
|
|
||||||
else -> RetrofitInstance.authApi.unauthenticatedSubscriptions(
|
else -> RetrofitInstance.authApi.unauthenticatedSubscriptions(
|
||||||
subscriptions.joinToString(",")
|
channelIds.joinToString(",")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getSubscriptionChannelIds(): List<String> {
|
||||||
|
return Database.localSubscriptionDao().getAll().map { it.channelId }
|
||||||
|
}
|
||||||
}
|
}
|
@ -4,19 +4,18 @@ import com.github.libretube.api.RetrofitInstance
|
|||||||
import com.github.libretube.api.SubscriptionHelper
|
import com.github.libretube.api.SubscriptionHelper
|
||||||
import com.github.libretube.api.SubscriptionHelper.GET_SUBSCRIPTIONS_LIMIT
|
import com.github.libretube.api.SubscriptionHelper.GET_SUBSCRIPTIONS_LIMIT
|
||||||
import com.github.libretube.api.obj.StreamItem
|
import com.github.libretube.api.obj.StreamItem
|
||||||
import com.github.libretube.extensions.toID
|
|
||||||
|
|
||||||
class PipedNoAccountFeedRepository: FeedRepository {
|
class PipedNoAccountFeedRepository: FeedRepository {
|
||||||
override suspend fun getFeed(forceRefresh: Boolean): List<StreamItem> {
|
override suspend fun getFeed(forceRefresh: Boolean): List<StreamItem> {
|
||||||
val subscriptions = SubscriptionHelper.getSubscriptions().map { it.url.toID() }
|
val channelIds = SubscriptionHelper.getSubscriptionChannelIds()
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
subscriptions.size > GET_SUBSCRIPTIONS_LIMIT ->
|
channelIds.size > GET_SUBSCRIPTIONS_LIMIT ->
|
||||||
RetrofitInstance.authApi
|
RetrofitInstance.authApi
|
||||||
.getUnauthenticatedFeed(subscriptions)
|
.getUnauthenticatedFeed(channelIds)
|
||||||
|
|
||||||
else -> RetrofitInstance.authApi.getUnauthenticatedFeed(
|
else -> RetrofitInstance.authApi.getUnauthenticatedFeed(
|
||||||
subscriptions.joinToString(",")
|
channelIds.joinToString(",")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,4 +8,5 @@ interface SubscriptionsRepository {
|
|||||||
suspend fun isSubscribed(channelId: String): Boolean?
|
suspend fun isSubscribed(channelId: String): Boolean?
|
||||||
suspend fun importSubscriptions(newChannels: List<String>)
|
suspend fun importSubscriptions(newChannels: List<String>)
|
||||||
suspend fun getSubscriptions(): List<Subscription>
|
suspend fun getSubscriptions(): List<Subscription>
|
||||||
|
suspend fun getSubscriptionChannelIds(): List<String>
|
||||||
}
|
}
|
@ -33,6 +33,7 @@ import com.github.libretube.enums.NotificationId
|
|||||||
import com.github.libretube.extensions.formatAsFileSize
|
import com.github.libretube.extensions.formatAsFileSize
|
||||||
import com.github.libretube.extensions.getContentLength
|
import com.github.libretube.extensions.getContentLength
|
||||||
import com.github.libretube.extensions.parcelableExtra
|
import com.github.libretube.extensions.parcelableExtra
|
||||||
|
import com.github.libretube.extensions.toLocalDate
|
||||||
import com.github.libretube.extensions.toastFromMainDispatcher
|
import com.github.libretube.extensions.toastFromMainDispatcher
|
||||||
import com.github.libretube.extensions.toastFromMainThread
|
import com.github.libretube.extensions.toastFromMainThread
|
||||||
import com.github.libretube.helpers.DownloadHelper
|
import com.github.libretube.helpers.DownloadHelper
|
||||||
@ -56,8 +57,6 @@ import kotlinx.coroutines.flow.SharedFlow
|
|||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.datetime.TimeZone
|
|
||||||
import kotlinx.datetime.toLocalDateTime
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@ -159,7 +158,7 @@ class DownloadService : LifecycleService() {
|
|||||||
streams.description,
|
streams.description,
|
||||||
streams.uploader,
|
streams.uploader,
|
||||||
streams.duration,
|
streams.duration,
|
||||||
streams.uploadTimestamp?.toLocalDateTime(TimeZone.currentSystemDefault())?.date,
|
streams.uploadTimestamp?.toLocalDate(),
|
||||||
thumbnailTargetPath
|
thumbnailTargetPath
|
||||||
)
|
)
|
||||||
Database.downloadDao().insertDownload(download)
|
Database.downloadDao().insertDownload(download)
|
||||||
|
@ -1059,7 +1059,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
// set media source and resolution in the beginning
|
// set media source and resolution in the beginning
|
||||||
updateResolution(commonPlayerViewModel.isFullscreen.value == true)
|
updateResolution(commonPlayerViewModel.isFullscreen.value == true)
|
||||||
|
|
||||||
if (streams.category == Streams.categoryMusic) {
|
if (streams.category == Streams.CATEGORY_MUSIC) {
|
||||||
playerController.setPlaybackSpeed(1f)
|
playerController.setPlaybackSpeed(1f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -118,7 +118,7 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment() {
|
|||||||
binding.subProgress.isVisible = true
|
binding.subProgress.isVisible = true
|
||||||
|
|
||||||
if (viewModel.videoFeed.value == null) {
|
if (viewModel.videoFeed.value == null) {
|
||||||
viewModel.fetchFeed(requireContext())
|
viewModel.fetchFeed(requireContext(), forceRefresh = false)
|
||||||
}
|
}
|
||||||
if (viewModel.subscriptions.value == null) {
|
if (viewModel.subscriptions.value == null) {
|
||||||
viewModel.fetchSubscriptions(requireContext())
|
viewModel.fetchSubscriptions(requireContext())
|
||||||
@ -134,7 +134,7 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment() {
|
|||||||
|
|
||||||
binding.subRefresh.setOnRefreshListener {
|
binding.subRefresh.setOnRefreshListener {
|
||||||
viewModel.fetchSubscriptions(requireContext())
|
viewModel.fetchSubscriptions(requireContext())
|
||||||
viewModel.fetchFeed(requireContext())
|
viewModel.fetchFeed(requireContext(), forceRefresh = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.toggleSubs.isVisible = true
|
binding.toggleSubs.isVisible = true
|
||||||
|
@ -18,7 +18,6 @@ import androidx.recyclerview.widget.GridLayoutManager
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.room.withTransaction
|
import androidx.room.withTransaction
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.api.obj.StreamItem
|
|
||||||
import com.github.libretube.constants.PreferenceKeys
|
import com.github.libretube.constants.PreferenceKeys
|
||||||
import com.github.libretube.databinding.FragmentWatchHistoryBinding
|
import com.github.libretube.databinding.FragmentWatchHistoryBinding
|
||||||
import com.github.libretube.db.DatabaseHelper
|
import com.github.libretube.db.DatabaseHelper
|
||||||
@ -180,18 +179,7 @@ class WatchHistoryFragment : DynamicLayoutManagerFragment() {
|
|||||||
binding.playAll.setOnClickListener {
|
binding.playAll.setOnClickListener {
|
||||||
PlayingQueue.resetToDefaults()
|
PlayingQueue.resetToDefaults()
|
||||||
PlayingQueue.add(
|
PlayingQueue.add(
|
||||||
*watchHistory.reversed().map {
|
*watchHistory.reversed().map(WatchHistoryItem::toStreamItem).toTypedArray()
|
||||||
StreamItem(
|
|
||||||
url = "/watch?v=${it.videoId}",
|
|
||||||
title = it.title,
|
|
||||||
thumbnail = it.thumbnailUrl,
|
|
||||||
uploaderName = it.uploader,
|
|
||||||
uploaderUrl = it.uploaderUrl,
|
|
||||||
uploaderAvatar = it.uploaderAvatar,
|
|
||||||
uploadedDate = it.uploadDate?.toString(),
|
|
||||||
duration = it.duration
|
|
||||||
)
|
|
||||||
}.toTypedArray()
|
|
||||||
)
|
)
|
||||||
NavigationHelper.navigateVideo(
|
NavigationHelper.navigateVideo(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
|
@ -122,7 +122,7 @@ class HomeViewModel : ViewModel() {
|
|||||||
private suspend fun tryLoadFeed(subscriptionsViewModel: SubscriptionsViewModel): List<StreamItem> {
|
private suspend fun tryLoadFeed(subscriptionsViewModel: SubscriptionsViewModel): List<StreamItem> {
|
||||||
subscriptionsViewModel.videoFeed.value?.let { return it }
|
subscriptionsViewModel.videoFeed.value?.let { return it }
|
||||||
|
|
||||||
val feed = SubscriptionHelper.getFeed()
|
val feed = SubscriptionHelper.getFeed(forceRefresh = false)
|
||||||
subscriptionsViewModel.videoFeed.postValue(feed)
|
subscriptionsViewModel.videoFeed.postValue(feed)
|
||||||
|
|
||||||
return if (hideWatched) feed.filterWatched() else feed
|
return if (hideWatched) feed.filterWatched() else feed
|
||||||
|
@ -21,10 +21,10 @@ class SubscriptionsViewModel : ViewModel() {
|
|||||||
|
|
||||||
var subscriptions = MutableLiveData<List<Subscription>?>()
|
var subscriptions = MutableLiveData<List<Subscription>?>()
|
||||||
|
|
||||||
fun fetchFeed(context: Context) {
|
fun fetchFeed(context: Context, forceRefresh: Boolean) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val videoFeed = try {
|
val videoFeed = try {
|
||||||
SubscriptionHelper.getFeed()
|
SubscriptionHelper.getFeed(forceRefresh = forceRefresh)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
context.toastFromMainDispatcher(R.string.server_error)
|
context.toastFromMainDispatcher(R.string.server_error)
|
||||||
Log.e(TAG(), e.toString())
|
Log.e(TAG(), e.toString())
|
||||||
|
@ -9,10 +9,9 @@ import androidx.core.text.isDigitsOnly
|
|||||||
import com.github.libretube.BuildConfig
|
import com.github.libretube.BuildConfig
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.extensions.formatShort
|
import com.github.libretube.extensions.formatShort
|
||||||
|
import com.github.libretube.extensions.toLocalDate
|
||||||
import com.google.common.math.IntMath.pow
|
import com.google.common.math.IntMath.pow
|
||||||
import kotlinx.datetime.TimeZone
|
|
||||||
import kotlinx.datetime.toJavaLocalDate
|
import kotlinx.datetime.toJavaLocalDate
|
||||||
import kotlinx.datetime.toLocalDateTime
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
@ -51,9 +50,7 @@ object TextUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun localizeInstant(instant: kotlinx.datetime.Instant): String {
|
fun localizeInstant(instant: kotlinx.datetime.Instant): String {
|
||||||
val date = instant.toLocalDateTime(TimeZone.currentSystemDefault()).date
|
return localizeDate(instant.toLocalDate())
|
||||||
|
|
||||||
return localizeDate(date)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -82,7 +82,7 @@ class NotificationWorker(appContext: Context, parameters: WorkerParameters) :
|
|||||||
// fetch the users feed
|
// fetch the users feed
|
||||||
val videoFeed = try {
|
val videoFeed = try {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
SubscriptionHelper.getFeed()
|
SubscriptionHelper.getFeed(forceRefresh = true)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return false
|
return false
|
||||||
|
@ -526,6 +526,8 @@
|
|||||||
<string name="dialog_play_offline_body">Would you like to play the video from the download folder?</string>
|
<string name="dialog_play_offline_body">Would you like to play the video from the download folder?</string>
|
||||||
<string name="view_count">%1$s views</string>
|
<string name="view_count">%1$s views</string>
|
||||||
<string name="delete_only_watched_videos">Only delete already watched videos</string>
|
<string name="delete_only_watched_videos">Only delete already watched videos</string>
|
||||||
|
<string name="local_feed_extraction">Local feed extraction</string>
|
||||||
|
<string name="local_feed_extraction_summary">Directly fetch the feed from YouTube. This may be significantly slower.</string>
|
||||||
|
|
||||||
<!-- Notification channel strings -->
|
<!-- Notification channel strings -->
|
||||||
<string name="download_channel_name">Download Service</string>
|
<string name="download_channel_name">Download Service</string>
|
||||||
|
@ -91,4 +91,15 @@
|
|||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
<PreferenceCategory app:title="@string/subscriptions">
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:icon="@drawable/ic_region"
|
||||||
|
android:summary="@string/local_feed_extraction_summary"
|
||||||
|
android:title="@string/local_feed_extraction"
|
||||||
|
app:key="local_feed_extraction" />
|
||||||
|
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
Loading…
x
Reference in New Issue
Block a user