mirror of
https://github.com/TeamNewPipe/NewPipe.git
synced 2025-10-03 09:49:21 +02:00
Track downloaded streams and surface status on video detail
This commit is contained in:
parent
abfde872f1
commit
b5cb367edb
22 changed files with 1949 additions and 13 deletions
856
app/schemas/org.schabi.newpipe.database.AppDatabase/10.json
Normal file
856
app/schemas/org.schabi.newpipe.database.AppDatabase/10.json
Normal file
|
@ -0,0 +1,856 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 10,
|
||||||
|
"identityHash": "92195bb0de0864bb1a0d7e4bbb16ec0f",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "subscriptions",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT, `notification_mode` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatarUrl",
|
||||||
|
"columnName": "avatar_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriberCount",
|
||||||
|
"columnName": "subscriber_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationMode",
|
||||||
|
"columnName": "notification_mode",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_subscriptions_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "search_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "creationDate",
|
||||||
|
"columnName": "creation_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "search",
|
||||||
|
"columnName": "search",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_search_history_search",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"search"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "streams",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `stream_type` TEXT NOT NULL, `duration` INTEGER NOT NULL, `uploader` TEXT NOT NULL, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamType",
|
||||||
|
"columnName": "stream_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderUrl",
|
||||||
|
"columnName": "uploader_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "viewCount",
|
||||||
|
"columnName": "view_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "textualUploadDate",
|
||||||
|
"columnName": "textual_upload_date",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploadDate",
|
||||||
|
"columnName": "upload_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isUploadDateApproximation",
|
||||||
|
"columnName": "is_upload_date_approximation",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_streams_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessDate",
|
||||||
|
"columnName": "access_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "repeatCount",
|
||||||
|
"columnName": "repeat_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"access_date"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_stream_history_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_state",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "progressMillis",
|
||||||
|
"columnName": "progress_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, `thumbnail_stream_id` INTEGER NOT NULL, `display_index` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isThumbnailPermanent",
|
||||||
|
"columnName": "is_thumbnail_permanent",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailStreamId",
|
||||||
|
"columnName": "thumbnail_stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayIndex",
|
||||||
|
"columnName": "display_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlist_stream_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistUid",
|
||||||
|
"columnName": "playlist_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "join_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_playlist_id_join_index",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "playlists",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"playlist_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "remote_playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `display_index` INTEGER NOT NULL, `stream_count` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayIndex",
|
||||||
|
"columnName": "display_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamCount",
|
||||||
|
"columnName": "stream_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `subscription_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamId",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortOrder",
|
||||||
|
"columnName": "sort_order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_sort_order",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"sort_order"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group_subscription_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`group_id`, `subscription_id`), FOREIGN KEY(`group_id`) REFERENCES `feed_group`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "feedGroupId",
|
||||||
|
"columnName": "group_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"group_id",
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_subscription_join_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "feed_group",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"group_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_last_updated",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subscription_id` INTEGER NOT NULL, `last_updated` INTEGER, PRIMARY KEY(`subscription_id`), FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUpdated",
|
||||||
|
"columnName": "last_updated",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "downloaded_streams",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `stream_uid` INTEGER NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `file_uri` TEXT NOT NULL, `parent_uri` TEXT, `display_name` TEXT, `mime` TEXT, `size_bytes` INTEGER, `quality_label` TEXT, `duration_ms` INTEGER, `status` INTEGER NOT NULL, `added_at` INTEGER NOT NULL, `last_checked_at` INTEGER, `missing_since` INTEGER, FOREIGN KEY(`stream_uid`) REFERENCES `streams`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "fileUri",
|
||||||
|
"columnName": "file_uri",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "parentUri",
|
||||||
|
"columnName": "parent_uri",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "display_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mime",
|
||||||
|
"columnName": "mime",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sizeBytes",
|
||||||
|
"columnName": "size_bytes",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "qualityLabel",
|
||||||
|
"columnName": "quality_label",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "durationMs",
|
||||||
|
"columnName": "duration_ms",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "status",
|
||||||
|
"columnName": "status",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "addedAt",
|
||||||
|
"columnName": "added_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastCheckedAt",
|
||||||
|
"columnName": "last_checked_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "missingSince",
|
||||||
|
"columnName": "missing_since",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_downloaded_streams_stream_uid",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_uid"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_downloaded_streams_stream_uid` ON `${TABLE_NAME}` (`stream_uid`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"stream_uid"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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, '92195bb0de0864bb1a0d7e4bbb16ec0f')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,6 +25,7 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||||
import org.acra.ACRA.init
|
import org.acra.ACRA.init
|
||||||
import org.acra.ACRA.isACRASenderServiceProcess
|
import org.acra.ACRA.isACRASenderServiceProcess
|
||||||
import org.acra.config.CoreConfigurationBuilder
|
import org.acra.config.CoreConfigurationBuilder
|
||||||
|
import org.schabi.newpipe.download.DownloadMaintenance
|
||||||
import org.schabi.newpipe.error.ReCaptchaActivity
|
import org.schabi.newpipe.error.ReCaptchaActivity
|
||||||
import org.schabi.newpipe.extractor.NewPipe
|
import org.schabi.newpipe.extractor.NewPipe
|
||||||
import org.schabi.newpipe.extractor.downloader.Downloader
|
import org.schabi.newpipe.extractor.downloader.Downloader
|
||||||
|
@ -120,6 +121,8 @@ open class App :
|
||||||
configureRxJavaErrorHandler()
|
configureRxJavaErrorHandler()
|
||||||
|
|
||||||
YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl)
|
YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl)
|
||||||
|
|
||||||
|
DownloadMaintenance.schedule(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun newImageLoader(context: Context): ImageLoader =
|
override fun newImageLoader(context: Context): ImageLoader =
|
||||||
|
|
|
@ -9,6 +9,7 @@ import static org.schabi.newpipe.database.Migrations.MIGRATION_5_6;
|
||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_6_7;
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_6_7;
|
||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_7_8;
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_7_8;
|
||||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_8_9;
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_8_9;
|
||||||
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_9_10;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
@ -29,7 +30,7 @@ public final class NewPipeDatabase {
|
||||||
return Room
|
return Room
|
||||||
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
|
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
|
||||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5,
|
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5,
|
||||||
MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9)
|
MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9, MIGRATION_9_10)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
package org.schabi.newpipe.database;
|
package org.schabi.newpipe.database;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.Migrations.DB_VER_9;
|
import static org.schabi.newpipe.database.Migrations.DB_VER_10;
|
||||||
|
|
||||||
import androidx.room.Database;
|
import androidx.room.Database;
|
||||||
import androidx.room.RoomDatabase;
|
import androidx.room.RoomDatabase;
|
||||||
import androidx.room.TypeConverters;
|
import androidx.room.TypeConverters;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.download.DownloadedStreamEntity;
|
||||||
|
import org.schabi.newpipe.database.download.DownloadedStreamsDao;
|
||||||
import org.schabi.newpipe.database.feed.dao.FeedDAO;
|
import org.schabi.newpipe.database.feed.dao.FeedDAO;
|
||||||
import org.schabi.newpipe.database.feed.dao.FeedGroupDAO;
|
import org.schabi.newpipe.database.feed.dao.FeedGroupDAO;
|
||||||
import org.schabi.newpipe.database.feed.model.FeedEntity;
|
import org.schabi.newpipe.database.feed.model.FeedEntity;
|
||||||
|
@ -36,9 +38,9 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||||
StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class,
|
StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class,
|
||||||
PlaylistEntity.class, PlaylistStreamEntity.class, PlaylistRemoteEntity.class,
|
PlaylistEntity.class, PlaylistStreamEntity.class, PlaylistRemoteEntity.class,
|
||||||
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
|
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
|
||||||
FeedLastUpdatedEntity.class
|
FeedLastUpdatedEntity.class, DownloadedStreamEntity.class
|
||||||
},
|
},
|
||||||
version = DB_VER_9
|
version = DB_VER_10
|
||||||
)
|
)
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
public static final String DATABASE_NAME = "newpipe.db";
|
public static final String DATABASE_NAME = "newpipe.db";
|
||||||
|
@ -62,4 +64,6 @@ public abstract class AppDatabase extends RoomDatabase {
|
||||||
public abstract FeedGroupDAO feedGroupDAO();
|
public abstract FeedGroupDAO feedGroupDAO();
|
||||||
|
|
||||||
public abstract SubscriptionDAO subscriptionDAO();
|
public abstract SubscriptionDAO subscriptionDAO();
|
||||||
|
|
||||||
|
public abstract DownloadedStreamsDao downloadedStreamsDao();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package org.schabi.newpipe.database
|
package org.schabi.newpipe.database
|
||||||
|
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
|
import org.schabi.newpipe.database.download.DownloadedStreamStatus
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType
|
import org.schabi.newpipe.extractor.stream.StreamType
|
||||||
import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
@ -49,4 +50,14 @@ class Converters {
|
||||||
fun feedGroupIconOf(id: Int): FeedGroupIcon {
|
fun feedGroupIconOf(id: Int): FeedGroupIcon {
|
||||||
return FeedGroupIcon.entries.first { it.id == id }
|
return FeedGroupIcon.entries.first { it.id == id }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun downloadedStreamStatusOf(value: Int?): DownloadedStreamStatus? {
|
||||||
|
return value?.let { DownloadedStreamStatus.fromValue(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun integerOf(status: DownloadedStreamStatus?): Int? {
|
||||||
|
return status?.value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ public final class Migrations {
|
||||||
public static final int DB_VER_7 = 7;
|
public static final int DB_VER_7 = 7;
|
||||||
public static final int DB_VER_8 = 8;
|
public static final int DB_VER_8 = 8;
|
||||||
public static final int DB_VER_9 = 9;
|
public static final int DB_VER_9 = 9;
|
||||||
|
public static final int DB_VER_10 = 10;
|
||||||
|
|
||||||
private static final String TAG = Migrations.class.getName();
|
private static final String TAG = Migrations.class.getName();
|
||||||
public static final boolean DEBUG = MainActivity.DEBUG;
|
public static final boolean DEBUG = MainActivity.DEBUG;
|
||||||
|
@ -302,6 +303,22 @@ public final class Migrations {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_9_10 = new Migration(DB_VER_9, DB_VER_10) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS downloaded_streams "
|
||||||
|
+ "(id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||||
|
+ "stream_uid INTEGER NOT NULL, service_id INTEGER NOT NULL, "
|
||||||
|
+ "url TEXT NOT NULL, file_uri TEXT NOT NULL, parent_uri TEXT, "
|
||||||
|
+ "display_name TEXT, mime TEXT, size_bytes INTEGER, quality_label TEXT, "
|
||||||
|
+ "duration_ms INTEGER, status INTEGER NOT NULL, added_at INTEGER NOT NULL, "
|
||||||
|
+ "last_checked_at INTEGER, missing_since INTEGER, FOREIGN KEY(stream_uid) "
|
||||||
|
+ "REFERENCES streams(uid) ON UPDATE CASCADE ON DELETE CASCADE)");
|
||||||
|
database.execSQL("CREATE UNIQUE INDEX index_downloaded_streams_stream_uid "
|
||||||
|
+ "ON downloaded_streams (stream_uid)");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private Migrations() {
|
private Migrations() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
package org.schabi.newpipe.database.download
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.Index
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_ADDED_AT
|
||||||
|
import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_DISPLAY_NAME
|
||||||
|
import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_DURATION_MS
|
||||||
|
import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_FILE_URI
|
||||||
|
import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_ID
|
||||||
|
import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_LAST_CHECKED_AT
|
||||||
|
import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_MIME
|
||||||
|
import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_MISSING_SINCE
|
||||||
|
import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_PARENT_URI
|
||||||
|
import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_QUALITY_LABEL
|
||||||
|
import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_SERVICE_ID
|
||||||
|
import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_SIZE_BYTES
|
||||||
|
import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_STATUS
|
||||||
|
import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_STREAM_UID
|
||||||
|
import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.COLUMN_URL
|
||||||
|
import org.schabi.newpipe.database.download.DownloadedStreamEntity.Companion.TABLE_NAME
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = TABLE_NAME,
|
||||||
|
indices = [Index(value = [COLUMN_STREAM_UID], unique = true)],
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = StreamEntity::class,
|
||||||
|
parentColumns = [StreamEntity.STREAM_ID],
|
||||||
|
childColumns = [COLUMN_STREAM_UID],
|
||||||
|
onDelete = ForeignKey.CASCADE
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data class DownloadedStreamEntity(
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
@ColumnInfo(name = COLUMN_ID)
|
||||||
|
var id: Long = 0,
|
||||||
|
|
||||||
|
@ColumnInfo(name = COLUMN_STREAM_UID)
|
||||||
|
var streamUid: Long,
|
||||||
|
|
||||||
|
@ColumnInfo(name = COLUMN_SERVICE_ID)
|
||||||
|
var serviceId: Int,
|
||||||
|
|
||||||
|
@ColumnInfo(name = COLUMN_URL)
|
||||||
|
var url: String,
|
||||||
|
|
||||||
|
@ColumnInfo(name = COLUMN_FILE_URI)
|
||||||
|
var fileUri: String,
|
||||||
|
|
||||||
|
@ColumnInfo(name = COLUMN_PARENT_URI)
|
||||||
|
var parentUri: String? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = COLUMN_DISPLAY_NAME)
|
||||||
|
var displayName: String? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = COLUMN_MIME)
|
||||||
|
var mime: String? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = COLUMN_SIZE_BYTES)
|
||||||
|
var sizeBytes: Long? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = COLUMN_QUALITY_LABEL)
|
||||||
|
var qualityLabel: String? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = COLUMN_DURATION_MS)
|
||||||
|
var durationMs: Long? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = COLUMN_STATUS)
|
||||||
|
var status: DownloadedStreamStatus,
|
||||||
|
|
||||||
|
@ColumnInfo(name = COLUMN_ADDED_AT)
|
||||||
|
var addedAt: Long,
|
||||||
|
|
||||||
|
@ColumnInfo(name = COLUMN_LAST_CHECKED_AT)
|
||||||
|
var lastCheckedAt: Long? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = COLUMN_MISSING_SINCE)
|
||||||
|
var missingSince: Long? = null
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
const val TABLE_NAME = "downloaded_streams"
|
||||||
|
const val COLUMN_ID = "id"
|
||||||
|
const val COLUMN_STREAM_UID = "stream_uid"
|
||||||
|
const val COLUMN_SERVICE_ID = "service_id"
|
||||||
|
const val COLUMN_URL = "url"
|
||||||
|
const val COLUMN_FILE_URI = "file_uri"
|
||||||
|
const val COLUMN_PARENT_URI = "parent_uri"
|
||||||
|
const val COLUMN_DISPLAY_NAME = "display_name"
|
||||||
|
const val COLUMN_MIME = "mime"
|
||||||
|
const val COLUMN_SIZE_BYTES = "size_bytes"
|
||||||
|
const val COLUMN_QUALITY_LABEL = "quality_label"
|
||||||
|
const val COLUMN_DURATION_MS = "duration_ms"
|
||||||
|
const val COLUMN_STATUS = "status"
|
||||||
|
const val COLUMN_ADDED_AT = "added_at"
|
||||||
|
const val COLUMN_LAST_CHECKED_AT = "last_checked_at"
|
||||||
|
const val COLUMN_MISSING_SINCE = "missing_since"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package org.schabi.newpipe.database.download
|
||||||
|
|
||||||
|
enum class DownloadedStreamStatus(val value: Int) {
|
||||||
|
IN_PROGRESS(0),
|
||||||
|
AVAILABLE(1),
|
||||||
|
MISSING(2),
|
||||||
|
UNLINKED(3);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromValue(value: Int): DownloadedStreamStatus = entries.firstOrNull {
|
||||||
|
it.value == value
|
||||||
|
} ?: IN_PROGRESS
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package org.schabi.newpipe.database.download
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import androidx.room.Update
|
||||||
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
|
import io.reactivex.rxjava3.core.Maybe
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface DownloadedStreamsDao {
|
||||||
|
@Query("SELECT * FROM downloaded_streams WHERE stream_uid = :streamUid LIMIT 1")
|
||||||
|
fun observeByStreamUid(streamUid: Long): Flowable<List<DownloadedStreamEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM downloaded_streams WHERE stream_uid = :streamUid LIMIT 1")
|
||||||
|
fun getByStreamUid(streamUid: Long): Maybe<DownloadedStreamEntity>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
fun insert(entity: DownloadedStreamEntity): Long
|
||||||
|
|
||||||
|
@Update
|
||||||
|
fun update(entity: DownloadedStreamEntity): Int
|
||||||
|
|
||||||
|
@Query("SELECT * FROM downloaded_streams WHERE stream_uid = :streamUid LIMIT 1")
|
||||||
|
fun findEntityByStreamUid(streamUid: Long): DownloadedStreamEntity?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM downloaded_streams WHERE id = :id LIMIT 1")
|
||||||
|
fun findEntityById(id: Long): DownloadedStreamEntity?
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
fun insertOrUpdate(entity: DownloadedStreamEntity): Long {
|
||||||
|
val newId = insert(entity)
|
||||||
|
if (newId != -1L) {
|
||||||
|
entity.id = newId
|
||||||
|
return newId
|
||||||
|
}
|
||||||
|
update(entity)
|
||||||
|
return entity.id
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("UPDATE downloaded_streams SET status = :status, last_checked_at = :lastCheckedAt, missing_since = :missingSince WHERE id = :id")
|
||||||
|
fun updateStatus(id: Long, status: DownloadedStreamStatus, lastCheckedAt: Long?, missingSince: Long?)
|
||||||
|
|
||||||
|
@Query("UPDATE downloaded_streams SET file_uri = :fileUri WHERE id = :id")
|
||||||
|
fun updateFileUri(id: Long, fileUri: String)
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
fun delete(entity: DownloadedStreamEntity)
|
||||||
|
|
||||||
|
@Query("DELETE FROM downloaded_streams WHERE stream_uid = :streamUid")
|
||||||
|
fun deleteByStreamUid(streamUid: Long): Int
|
||||||
|
|
||||||
|
@Query("SELECT * FROM downloaded_streams WHERE status = :status ORDER BY last_checked_at ASC LIMIT :limit")
|
||||||
|
fun listByStatus(status: DownloadedStreamStatus, limit: Int): List<DownloadedStreamEntity>
|
||||||
|
}
|
|
@ -81,6 +81,14 @@ public class DownloadActivity extends AppCompatActivity {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
new Thread(() ->
|
||||||
|
DownloadMaintenance.revalidateAvailable(DownloadActivity.this, 10)
|
||||||
|
).start();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
package org.schabi.newpipe.download
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import org.schabi.newpipe.BuildConfig
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object DownloadAvailabilityChecker {
|
||||||
|
private const val TAG = "DownloadAvailabilityChecker"
|
||||||
|
|
||||||
|
fun isReadable(context: Context, uri: Uri): Boolean {
|
||||||
|
val scheme = uri.scheme
|
||||||
|
return when {
|
||||||
|
scheme.equals("file", ignoreCase = true) ->
|
||||||
|
File(uri.path ?: return false).canRead()
|
||||||
|
scheme.equals("content", ignoreCase = true) ->
|
||||||
|
probeContentUri(context, uri)
|
||||||
|
else -> probeContentUri(context, uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun probeContentUri(context: Context, uri: Uri): Boolean {
|
||||||
|
return try {
|
||||||
|
context.contentResolver.openAssetFileDescriptor(uri, "r")?.use { true } ?: false
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.w(TAG, "Failed to probe availability for $uri", throwable)
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -79,7 +79,9 @@ import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import us.shandian.giga.get.MissionRecoveryInfo;
|
import us.shandian.giga.get.MissionRecoveryInfo;
|
||||||
import us.shandian.giga.postprocessing.Postprocessing;
|
import us.shandian.giga.postprocessing.Postprocessing;
|
||||||
|
@ -1132,12 +1134,70 @@ public class DownloadDialog extends DialogFragment
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
DownloadManagerService.startMission(context, urls, storage, kind, threads,
|
final String qualityLabel = buildQualityLabel(selectedStream);
|
||||||
currentInfo.getUrl(), psName, psArgs, nearLength, new ArrayList<>(recoveryInfo));
|
final MediaFormat selectedFormat = selectedStream.getFormat();
|
||||||
|
final String resolvedMime = selectedFormat != null ? selectedFormat.getMimeType()
|
||||||
|
: storage.getType();
|
||||||
|
final Long durationMs = currentInfo.getDuration() > 0
|
||||||
|
? TimeUnit.SECONDS.toMillis(currentInfo.getDuration()) : null;
|
||||||
|
final Long estimatedSize = nearLength > 0 ? nearLength : null;
|
||||||
|
|
||||||
Toast.makeText(context, getString(R.string.download_has_started),
|
final char missionKind = kind;
|
||||||
Toast.LENGTH_SHORT).show();
|
final int missionThreads = threads;
|
||||||
|
final String missionSourceUrl = currentInfo.getUrl();
|
||||||
|
final String missionPsName = psName;
|
||||||
|
final String[] missionPsArgs = psArgs;
|
||||||
|
final long missionNearLength = nearLength;
|
||||||
|
|
||||||
dismiss();
|
disposables.add(DownloadedStreamsRepository.INSTANCE
|
||||||
|
.upsertForEnqueued(requireContext(), currentInfo, storage, null, resolvedMime,
|
||||||
|
qualityLabel, durationMs, estimatedSize)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(association -> {
|
||||||
|
DownloadManagerService.startMission(
|
||||||
|
context,
|
||||||
|
urls,
|
||||||
|
storage,
|
||||||
|
missionKind,
|
||||||
|
missionThreads,
|
||||||
|
missionSourceUrl,
|
||||||
|
missionPsName,
|
||||||
|
missionPsArgs,
|
||||||
|
missionNearLength,
|
||||||
|
new ArrayList<>(recoveryInfo),
|
||||||
|
association.getStreamUid(),
|
||||||
|
association.getEntityId(),
|
||||||
|
currentInfo.getServiceId()
|
||||||
|
);
|
||||||
|
|
||||||
|
Toast.makeText(context, getString(R.string.download_has_started),
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
|
||||||
|
dismiss();
|
||||||
|
},
|
||||||
|
throwable -> ErrorUtil.createNotification(requireContext(),
|
||||||
|
new ErrorInfo(throwable, UserAction.DOWNLOAD_FAILED,
|
||||||
|
"Preparing download metadata", currentInfo))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private String buildQualityLabel(@NonNull final Stream stream) {
|
||||||
|
if (stream instanceof VideoStream) {
|
||||||
|
return ((VideoStream) stream).getResolution();
|
||||||
|
} else if (stream instanceof AudioStream) {
|
||||||
|
final int bitrate = ((AudioStream) stream).getAverageBitrate();
|
||||||
|
return bitrate > 0 ? bitrate + "kbps" : null;
|
||||||
|
} else if (stream instanceof SubtitlesStream) {
|
||||||
|
final SubtitlesStream subtitlesStream = (SubtitlesStream) stream;
|
||||||
|
final String language = subtitlesStream.getDisplayLanguageName();
|
||||||
|
if (subtitlesStream.isAutoGenerated()) {
|
||||||
|
return language + " (" + getString(R.string.caption_auto_generated) + ")";
|
||||||
|
}
|
||||||
|
return language;
|
||||||
|
}
|
||||||
|
|
||||||
|
final MediaFormat format = stream.getFormat();
|
||||||
|
return format != null ? format.getSuffix() : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
package org.schabi.newpipe.download
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import org.schabi.newpipe.NewPipeDatabase
|
||||||
|
import org.schabi.newpipe.database.download.DownloadedStreamStatus
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
object DownloadMaintenance {
|
||||||
|
private const val WORK_NAME = "download_revalidation"
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun revalidateAvailable(context: Context, limit: Int = 25) {
|
||||||
|
val dao = NewPipeDatabase.getInstance(context).downloadedStreamsDao()
|
||||||
|
val entries = dao.listByStatus(DownloadedStreamStatus.AVAILABLE, limit)
|
||||||
|
if (entries.isEmpty()) return
|
||||||
|
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
for (entry in entries) {
|
||||||
|
val uriString = entry.fileUri
|
||||||
|
if (uriString.isBlank()) {
|
||||||
|
dao.updateStatus(entry.id, DownloadedStreamStatus.MISSING, now, entry.missingSince ?: now)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val available = DownloadAvailabilityChecker.isReadable(context, Uri.parse(uriString))
|
||||||
|
if (available) {
|
||||||
|
dao.updateStatus(entry.id, DownloadedStreamStatus.AVAILABLE, now, null)
|
||||||
|
} else {
|
||||||
|
dao.updateStatus(entry.id, DownloadedStreamStatus.MISSING, now, entry.missingSince ?: now)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun schedule(context: Context) {
|
||||||
|
val workRequest = PeriodicWorkRequestBuilder<DownloadRevalidationWorker>(1, TimeUnit.DAYS)
|
||||||
|
.build()
|
||||||
|
WorkManager.getInstance(context)
|
||||||
|
.enqueueUniquePeriodicWork(WORK_NAME, ExistingPeriodicWorkPolicy.KEEP, workRequest)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package org.schabi.newpipe.download
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.work.Worker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import org.schabi.newpipe.BuildConfig
|
||||||
|
|
||||||
|
class DownloadRevalidationWorker(
|
||||||
|
appContext: Context,
|
||||||
|
workerParams: WorkerParameters,
|
||||||
|
) : Worker(appContext, workerParams) {
|
||||||
|
|
||||||
|
override fun doWork(): Result {
|
||||||
|
return try {
|
||||||
|
DownloadMaintenance.revalidateAvailable(applicationContext)
|
||||||
|
Result.success()
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.e(TAG, "Failed to revalidate downloads", throwable)
|
||||||
|
}
|
||||||
|
Result.retry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
private const val TAG = "DownloadRevalidation"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,238 @@
|
||||||
|
package org.schabi.newpipe.download
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import io.reactivex.rxjava3.core.Completable
|
||||||
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
|
import io.reactivex.rxjava3.core.Maybe
|
||||||
|
import io.reactivex.rxjava3.core.Single
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import org.schabi.newpipe.NewPipeDatabase
|
||||||
|
import org.schabi.newpipe.database.AppDatabase
|
||||||
|
import org.schabi.newpipe.database.download.DownloadedStreamEntity
|
||||||
|
import org.schabi.newpipe.database.download.DownloadedStreamStatus
|
||||||
|
import org.schabi.newpipe.database.download.DownloadedStreamsDao
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||||
|
import org.schabi.newpipe.streams.io.StoredFileHelper
|
||||||
|
|
||||||
|
object DownloadedStreamsRepository {
|
||||||
|
|
||||||
|
data class DownloadAssociation(
|
||||||
|
val streamUid: Long,
|
||||||
|
val entityId: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun database(context: Context): AppDatabase {
|
||||||
|
return NewPipeDatabase.getInstance(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun downloadedDao(context: Context): DownloadedStreamsDao {
|
||||||
|
return database(context).downloadedStreamsDao()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun observeByStreamUid(context: Context, streamUid: Long): Flowable<List<DownloadedStreamEntity>> {
|
||||||
|
return downloadedDao(context)
|
||||||
|
.observeByStreamUid(streamUid)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getByStreamUid(context: Context, streamUid: Long): Maybe<DownloadedStreamEntity> {
|
||||||
|
return downloadedDao(context)
|
||||||
|
.getByStreamUid(streamUid)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ensureStreamEntry(context: Context, info: StreamInfo): Single<Long> {
|
||||||
|
return Single.fromCallable {
|
||||||
|
database(context).streamDAO().upsert(StreamEntity(info))
|
||||||
|
}.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun upsertForEnqueued(
|
||||||
|
context: Context,
|
||||||
|
info: StreamInfo,
|
||||||
|
storage: StoredFileHelper,
|
||||||
|
displayName: String?,
|
||||||
|
mime: String?,
|
||||||
|
qualityLabel: String?,
|
||||||
|
durationMs: Long?,
|
||||||
|
sizeBytes: Long?
|
||||||
|
): Single<DownloadAssociation> {
|
||||||
|
return Single.fromCallable {
|
||||||
|
val db = database(context)
|
||||||
|
db.runInTransaction<DownloadAssociation> {
|
||||||
|
val streamDao = db.streamDAO()
|
||||||
|
val dao = db.downloadedStreamsDao()
|
||||||
|
val streamId = streamDao.upsert(StreamEntity(info))
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
val fileUri = storage.uriString()
|
||||||
|
val entity = dao.findEntityByStreamUid(streamId)
|
||||||
|
val resolvedDisplayName = displayName ?: storage.getName()
|
||||||
|
val resolvedMime = mime ?: storage.getType()
|
||||||
|
|
||||||
|
if (entity == null) {
|
||||||
|
val newEntity = DownloadedStreamEntity(
|
||||||
|
streamUid = streamId,
|
||||||
|
serviceId = info.serviceId,
|
||||||
|
url = info.url,
|
||||||
|
fileUri = fileUri,
|
||||||
|
parentUri = storage.parentUriString(),
|
||||||
|
displayName = resolvedDisplayName,
|
||||||
|
mime = resolvedMime,
|
||||||
|
sizeBytes = sizeBytes,
|
||||||
|
qualityLabel = qualityLabel,
|
||||||
|
durationMs = durationMs,
|
||||||
|
status = DownloadedStreamStatus.IN_PROGRESS,
|
||||||
|
addedAt = now,
|
||||||
|
lastCheckedAt = null,
|
||||||
|
missingSince = null
|
||||||
|
)
|
||||||
|
val insertedId = dao.insert(newEntity)
|
||||||
|
val resolvedId = if (insertedId == -1L) {
|
||||||
|
dao.findEntityByStreamUid(streamId)?.id
|
||||||
|
?: throw IllegalStateException("Failed to resolve downloaded stream entry")
|
||||||
|
} else {
|
||||||
|
insertedId
|
||||||
|
}
|
||||||
|
newEntity.id = resolvedId
|
||||||
|
DownloadAssociation(streamId, resolvedId)
|
||||||
|
} else {
|
||||||
|
entity.serviceId = info.serviceId
|
||||||
|
entity.url = info.url
|
||||||
|
entity.fileUri = fileUri
|
||||||
|
val parentUri = storage.parentUriString()
|
||||||
|
if (parentUri != null) {
|
||||||
|
entity.parentUri = parentUri
|
||||||
|
}
|
||||||
|
entity.displayName = resolvedDisplayName
|
||||||
|
entity.mime = resolvedMime
|
||||||
|
entity.sizeBytes = sizeBytes
|
||||||
|
entity.qualityLabel = qualityLabel
|
||||||
|
entity.durationMs = durationMs
|
||||||
|
entity.status = DownloadedStreamStatus.IN_PROGRESS
|
||||||
|
entity.lastCheckedAt = null
|
||||||
|
entity.missingSince = null
|
||||||
|
if (entity.addedAt <= 0) {
|
||||||
|
entity.addedAt = now
|
||||||
|
}
|
||||||
|
dao.update(entity)
|
||||||
|
DownloadAssociation(streamId, entity.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun markFinished(
|
||||||
|
context: Context,
|
||||||
|
association: DownloadAssociation,
|
||||||
|
serviceId: Int,
|
||||||
|
url: String,
|
||||||
|
storage: StoredFileHelper,
|
||||||
|
mime: String?,
|
||||||
|
qualityLabel: String?,
|
||||||
|
durationMs: Long?,
|
||||||
|
sizeBytes: Long?
|
||||||
|
): Completable {
|
||||||
|
return Completable.fromAction {
|
||||||
|
val dao = downloadedDao(context)
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
val entity = dao.findEntityById(association.entityId)
|
||||||
|
?: dao.findEntityByStreamUid(association.streamUid)
|
||||||
|
?: DownloadedStreamEntity(
|
||||||
|
streamUid = association.streamUid,
|
||||||
|
serviceId = serviceId,
|
||||||
|
url = url,
|
||||||
|
fileUri = storage.uriString(),
|
||||||
|
parentUri = storage.parentUriString(),
|
||||||
|
displayName = storage.getName(),
|
||||||
|
mime = mime ?: storage.getType(),
|
||||||
|
sizeBytes = sizeBytes,
|
||||||
|
qualityLabel = qualityLabel,
|
||||||
|
durationMs = durationMs,
|
||||||
|
status = DownloadedStreamStatus.IN_PROGRESS,
|
||||||
|
addedAt = now
|
||||||
|
)
|
||||||
|
entity.serviceId = serviceId
|
||||||
|
entity.url = url
|
||||||
|
entity.fileUri = storage.uriString()
|
||||||
|
storage.parentUriString()?.let { entity.parentUri = it }
|
||||||
|
entity.displayName = storage.getName()
|
||||||
|
val resolvedMime = mime ?: storage.getType() ?: entity.mime
|
||||||
|
entity.mime = resolvedMime
|
||||||
|
entity.sizeBytes = sizeBytes ?: storage.safeLength() ?: entity.sizeBytes
|
||||||
|
if (qualityLabel != null) {
|
||||||
|
entity.qualityLabel = qualityLabel
|
||||||
|
}
|
||||||
|
if (durationMs != null) {
|
||||||
|
entity.durationMs = durationMs
|
||||||
|
}
|
||||||
|
entity.status = DownloadedStreamStatus.AVAILABLE
|
||||||
|
entity.lastCheckedAt = now
|
||||||
|
entity.missingSince = null
|
||||||
|
if (entity.addedAt <= 0) {
|
||||||
|
entity.addedAt = now
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity.id == 0L) {
|
||||||
|
val newId = dao.insert(entity)
|
||||||
|
entity.id = newId
|
||||||
|
} else {
|
||||||
|
dao.update(entity)
|
||||||
|
}
|
||||||
|
}.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateStatus(
|
||||||
|
context: Context,
|
||||||
|
entityId: Long,
|
||||||
|
status: DownloadedStreamStatus,
|
||||||
|
lastCheckedAt: Long? = System.currentTimeMillis(),
|
||||||
|
missingSince: Long? = null
|
||||||
|
): Completable {
|
||||||
|
return Completable.fromAction {
|
||||||
|
downloadedDao(context).updateStatus(entityId, status, lastCheckedAt, missingSince)
|
||||||
|
}.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateFileUri(context: Context, entityId: Long, uri: Uri): Completable {
|
||||||
|
return Completable.fromAction {
|
||||||
|
downloadedDao(context).updateFileUri(entityId, uri.toString())
|
||||||
|
}.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun relink(context: Context, entity: DownloadedStreamEntity, uri: Uri): Completable {
|
||||||
|
return Single.fromCallable {
|
||||||
|
StoredFileHelper(context, uri, entity.mime ?: StoredFileHelper.DEFAULT_MIME)
|
||||||
|
}.flatMapCompletable { helper ->
|
||||||
|
val association = DownloadAssociation(entity.streamUid, entity.id)
|
||||||
|
markFinished(
|
||||||
|
context,
|
||||||
|
association,
|
||||||
|
entity.serviceId,
|
||||||
|
entity.url,
|
||||||
|
helper,
|
||||||
|
helper.type,
|
||||||
|
entity.qualityLabel,
|
||||||
|
entity.durationMs,
|
||||||
|
helper.safeLength()
|
||||||
|
)
|
||||||
|
}.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteByStreamUid(context: Context, streamUid: Long): Completable {
|
||||||
|
return Completable.fromAction {
|
||||||
|
downloadedDao(context).deleteByStreamUid(streamUid)
|
||||||
|
}.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun StoredFileHelper.uriString(): String = getUri().toString()
|
||||||
|
|
||||||
|
private fun StoredFileHelper.safeLength(): Long? {
|
||||||
|
return runCatching { length() }.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun StoredFileHelper.parentUriString(): String? {
|
||||||
|
return runCatching { getParentUri() }.getOrNull()?.toString()
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import android.animation.ValueAnimator
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
|
@ -12,14 +13,17 @@ import android.content.pm.ActivityInfo
|
||||||
import android.database.ContentObserver
|
import android.database.ContentObserver
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.provider.DocumentsContract
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.util.DisplayMetrics
|
import android.util.DisplayMetrics
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
|
import android.view.ContextThemeWrapper
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -31,7 +35,9 @@ import android.view.WindowManager
|
||||||
import android.view.animation.DecelerateInterpolator
|
import android.view.animation.DecelerateInterpolator
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.AttrRes
|
import androidx.annotation.AttrRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
@ -44,6 +50,7 @@ import androidx.core.net.toUri
|
||||||
import androidx.core.os.postDelayed
|
import androidx.core.os.postDelayed
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import coil3.util.CoilUtils
|
import coil3.util.CoilUtils
|
||||||
import com.evernote.android.state.State
|
import com.evernote.android.state.State
|
||||||
|
@ -52,15 +59,22 @@ import com.google.android.exoplayer2.PlaybackParameters
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
|
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
import io.reactivex.rxjava3.disposables.Disposable
|
import io.reactivex.rxjava3.disposables.Disposable
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import org.schabi.newpipe.App
|
import org.schabi.newpipe.App
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.database.download.DownloadedStreamEntity
|
||||||
|
import org.schabi.newpipe.database.download.DownloadedStreamStatus
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
|
import org.schabi.newpipe.databinding.DownloadStatusSheetBinding
|
||||||
import org.schabi.newpipe.databinding.FragmentVideoDetailBinding
|
import org.schabi.newpipe.databinding.FragmentVideoDetailBinding
|
||||||
|
import org.schabi.newpipe.download.DownloadActivity
|
||||||
|
import org.schabi.newpipe.download.DownloadAvailabilityChecker
|
||||||
import org.schabi.newpipe.download.DownloadDialog
|
import org.schabi.newpipe.download.DownloadDialog
|
||||||
|
import org.schabi.newpipe.download.DownloadedStreamsRepository
|
||||||
import org.schabi.newpipe.error.ErrorInfo
|
import org.schabi.newpipe.error.ErrorInfo
|
||||||
import org.schabi.newpipe.error.ErrorUtil.Companion.showSnackbar
|
import org.schabi.newpipe.error.ErrorUtil.Companion.showSnackbar
|
||||||
import org.schabi.newpipe.error.ErrorUtil.Companion.showUiErrorSnackbar
|
import org.schabi.newpipe.error.ErrorUtil.Companion.showUiErrorSnackbar
|
||||||
|
@ -115,6 +129,7 @@ import org.schabi.newpipe.util.ThemeHelper
|
||||||
import org.schabi.newpipe.util.external_communication.KoreUtils
|
import org.schabi.newpipe.util.external_communication.KoreUtils
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||||
import org.schabi.newpipe.util.image.CoilHelper
|
import org.schabi.newpipe.util.image.CoilHelper
|
||||||
|
import java.io.File
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
@ -181,6 +196,17 @@ class VideoDetailFragment :
|
||||||
private var currentWorker: Disposable? = null
|
private var currentWorker: Disposable? = null
|
||||||
private val disposables = CompositeDisposable()
|
private val disposables = CompositeDisposable()
|
||||||
private var positionSubscriber: Disposable? = null
|
private var positionSubscriber: Disposable? = null
|
||||||
|
private var downloadStatusDisposable: Disposable? = null
|
||||||
|
private var currentStreamUid: Long? = null
|
||||||
|
private var currentDownloadedStream: DownloadedStreamEntity? = null
|
||||||
|
private var pendingRelinkEntity: DownloadedStreamEntity? = null
|
||||||
|
|
||||||
|
private val relinkLauncher = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
|
||||||
|
if (uri != null && pendingRelinkEntity != null) {
|
||||||
|
handleRelinkResult(pendingRelinkEntity!!, uri)
|
||||||
|
}
|
||||||
|
pendingRelinkEntity = null
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Service management
|
// Service management
|
||||||
|
@ -348,6 +374,13 @@ class VideoDetailFragment :
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
|
downloadStatusDisposable?.let {
|
||||||
|
disposables.remove(it)
|
||||||
|
it.dispose()
|
||||||
|
}
|
||||||
|
downloadStatusDisposable = null
|
||||||
|
currentDownloadedStream = null
|
||||||
|
currentStreamUid = null
|
||||||
nullableBinding = null
|
nullableBinding = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1366,6 +1399,9 @@ class VideoDetailFragment :
|
||||||
currentInfo = info
|
currentInfo = info
|
||||||
setInitialData(info.serviceId, info.originalUrl, info.name, playQueue)
|
setInitialData(info.serviceId, info.originalUrl, info.name, playQueue)
|
||||||
|
|
||||||
|
updateDownloadChip(null)
|
||||||
|
observeDownloadStatus(info)
|
||||||
|
|
||||||
updateTabs(info)
|
updateTabs(info)
|
||||||
|
|
||||||
binding.detailThumbnailPlayButton.animate(true, 200)
|
binding.detailThumbnailPlayButton.animate(true, 200)
|
||||||
|
@ -1544,6 +1580,269 @@ class VideoDetailFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun observeDownloadStatus(info: StreamInfo) {
|
||||||
|
val context = context ?: return
|
||||||
|
downloadStatusDisposable?.let {
|
||||||
|
disposables.remove(it)
|
||||||
|
it.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
val disposable = DownloadedStreamsRepository.ensureStreamEntry(context, info)
|
||||||
|
.flatMapPublisher { streamUid: Long ->
|
||||||
|
currentStreamUid = streamUid
|
||||||
|
DownloadedStreamsRepository.observeByStreamUid(context, streamUid)
|
||||||
|
}
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{ entities: List<DownloadedStreamEntity> ->
|
||||||
|
val entity = entities.firstOrNull()
|
||||||
|
updateDownloadChip(entity)
|
||||||
|
},
|
||||||
|
{ throwable ->
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.e(TAG, "Failed to observe download state", throwable)
|
||||||
|
}
|
||||||
|
updateDownloadChip(null)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
downloadStatusDisposable = disposable
|
||||||
|
disposables.add(disposable)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateDownloadChip(entity: DownloadedStreamEntity?) {
|
||||||
|
if (nullableBinding == null) return
|
||||||
|
|
||||||
|
currentDownloadedStream = entity
|
||||||
|
val chip = binding.detailDownloadStatusChip ?: return
|
||||||
|
|
||||||
|
if (entity == null || entity.status == DownloadedStreamStatus.UNLINKED) {
|
||||||
|
chip.isGone = true
|
||||||
|
chip.setOnClickListener(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
chip.isVisible = true
|
||||||
|
when (entity.status) {
|
||||||
|
DownloadedStreamStatus.IN_PROGRESS -> {
|
||||||
|
chip.text = getString(R.string.download_status_downloading)
|
||||||
|
chip.setOnClickListener { openDownloadsActivity() }
|
||||||
|
}
|
||||||
|
DownloadedStreamStatus.AVAILABLE,
|
||||||
|
DownloadedStreamStatus.MISSING -> {
|
||||||
|
chip.text = buildDownloadedLabel(entity)
|
||||||
|
chip.setOnClickListener { showDownloadOptions(entity) }
|
||||||
|
}
|
||||||
|
DownloadedStreamStatus.UNLINKED -> {
|
||||||
|
chip.isGone = true
|
||||||
|
chip.setOnClickListener(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildDownloadedLabel(entity: DownloadedStreamEntity): String {
|
||||||
|
val quality = entity.qualityLabel?.takeIf { it.isNotBlank() }
|
||||||
|
return if (quality != null) {
|
||||||
|
getString(R.string.download_status_downloaded, quality)
|
||||||
|
} else {
|
||||||
|
getString(R.string.download_status_downloaded_simple)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showDownloadOptions(entity: DownloadedStreamEntity) {
|
||||||
|
val baseContext = requireContext()
|
||||||
|
val dialogTheme = ThemeHelper.getDialogTheme(baseContext)
|
||||||
|
val themedContext = ContextThemeWrapper(baseContext, dialogTheme)
|
||||||
|
val sheetBinding = DownloadStatusSheetBinding.inflate(LayoutInflater.from(themedContext))
|
||||||
|
val dialog = BottomSheetDialog(themedContext)
|
||||||
|
dialog.setContentView(sheetBinding.root)
|
||||||
|
|
||||||
|
val primaryTextColor = ThemeHelper.resolveColorFromAttr(themedContext, android.R.attr.textColorPrimary)
|
||||||
|
val secondaryTextColor = ThemeHelper.resolveColorFromAttr(themedContext, android.R.attr.textColorSecondary)
|
||||||
|
val backgroundDrawable = ThemeHelper.resolveDrawable(themedContext, android.R.attr.windowBackground)
|
||||||
|
val rippleDrawable = ThemeHelper.resolveDrawable(themedContext, R.attr.selector)
|
||||||
|
val accentColor = ThemeHelper.resolveColorFromAttr(themedContext, androidx.appcompat.R.attr.colorAccent)
|
||||||
|
|
||||||
|
sheetBinding.root.background = backgroundDrawable
|
||||||
|
sheetBinding.downloadStatusTitle.setTextColor(primaryTextColor)
|
||||||
|
sheetBinding.downloadStatusSubtitle.setTextColor(secondaryTextColor)
|
||||||
|
|
||||||
|
fun styleAction(textView: TextView) {
|
||||||
|
textView.setTextColor(primaryTextColor)
|
||||||
|
textView.background = rippleDrawable
|
||||||
|
}
|
||||||
|
|
||||||
|
styleAction(sheetBinding.downloadStatusOpen)
|
||||||
|
styleAction(sheetBinding.downloadStatusDelete)
|
||||||
|
styleAction(sheetBinding.downloadStatusShowInFolder)
|
||||||
|
sheetBinding.downloadStatusRemoveLink.apply {
|
||||||
|
setTextColor(accentColor)
|
||||||
|
background = rippleDrawable
|
||||||
|
}
|
||||||
|
|
||||||
|
val fileAvailable = entity.fileUri.takeUnless { it.isBlank() }
|
||||||
|
?.let { DownloadAvailabilityChecker.isReadable(baseContext, Uri.parse(it)) }
|
||||||
|
?: false
|
||||||
|
|
||||||
|
val title = entity.displayName?.takeIf { it.isNotBlank() }
|
||||||
|
?: currentInfo?.name
|
||||||
|
?: getString(R.string.download)
|
||||||
|
sheetBinding.downloadStatusTitle.text = title
|
||||||
|
|
||||||
|
val subtitleParts = mutableListOf<String>()
|
||||||
|
entity.qualityLabel?.takeIf { it.isNotBlank() }?.let(subtitleParts::add)
|
||||||
|
if (!fileAvailable) {
|
||||||
|
subtitleParts.add(getString(R.string.download_status_missing))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subtitleParts.isEmpty()) {
|
||||||
|
sheetBinding.downloadStatusSubtitle.isGone = true
|
||||||
|
} else {
|
||||||
|
sheetBinding.downloadStatusSubtitle.isVisible = true
|
||||||
|
sheetBinding.downloadStatusSubtitle.text = subtitleParts.joinToString(" • ")
|
||||||
|
}
|
||||||
|
|
||||||
|
sheetBinding.downloadStatusOpen.text = getString(R.string.download_action_open)
|
||||||
|
sheetBinding.downloadStatusDelete.text = getString(R.string.download_action_delete)
|
||||||
|
sheetBinding.downloadStatusShowInFolder.text = getString(R.string.download_action_show_in_folder)
|
||||||
|
sheetBinding.downloadStatusRemoveLink.text = getString(R.string.download_action_remove_link)
|
||||||
|
|
||||||
|
sheetBinding.downloadStatusOpen.isVisible = fileAvailable
|
||||||
|
sheetBinding.downloadStatusDelete.isVisible = fileAvailable
|
||||||
|
sheetBinding.downloadStatusShowInFolder.isVisible = fileAvailable && !entity.parentUri.isNullOrBlank()
|
||||||
|
sheetBinding.downloadStatusRemoveLink.isVisible = true
|
||||||
|
|
||||||
|
sheetBinding.downloadStatusOpen.setOnClickListener {
|
||||||
|
dialog.dismiss()
|
||||||
|
openDownloaded(entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
sheetBinding.downloadStatusDelete.setOnClickListener {
|
||||||
|
dialog.dismiss()
|
||||||
|
deleteDownloadedFile(entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
sheetBinding.downloadStatusShowInFolder.setOnClickListener {
|
||||||
|
dialog.dismiss()
|
||||||
|
showInFolder(entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
sheetBinding.downloadStatusRemoveLink.setOnClickListener {
|
||||||
|
dialog.dismiss()
|
||||||
|
removeDownloadAssociation(entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openDownloaded(entity: DownloadedStreamEntity) {
|
||||||
|
val uri = entity.fileUri.takeUnless { it.isBlank() }?.let(Uri::parse) ?: return
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
setDataAndType(uri, entity.mime ?: "*/*")
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
runCatching { startActivity(intent) }
|
||||||
|
.onFailure {
|
||||||
|
if (DEBUG) Log.e(TAG, "Failed to open downloaded file", it)
|
||||||
|
Toast.makeText(requireContext(), R.string.download_open_failed, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showInFolder(entity: DownloadedStreamEntity) {
|
||||||
|
val parent = entity.parentUri?.takeIf { it.isNotBlank() }?.let(Uri::parse)
|
||||||
|
if (parent == null) {
|
||||||
|
Toast.makeText(requireContext(), R.string.download_folder_open_failed, Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
setDataAndType(parent, DocumentsContract.Document.MIME_TYPE_DIR)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
runCatching { startActivity(intent) }
|
||||||
|
.onFailure {
|
||||||
|
val treeIntent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
||||||
|
putExtra(DocumentsContract.EXTRA_INITIAL_URI, parent)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
runCatching { startActivity(treeIntent) }
|
||||||
|
.onFailure { throwable ->
|
||||||
|
if (DEBUG) Log.e(TAG, "Failed to open folder", throwable)
|
||||||
|
Toast.makeText(requireContext(), R.string.download_folder_open_failed, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeDownloadAssociation(entity: DownloadedStreamEntity) {
|
||||||
|
val context = requireContext()
|
||||||
|
disposables.add(
|
||||||
|
DownloadedStreamsRepository.deleteByStreamUid(context, entity.streamUid)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{ Toast.makeText(context, R.string.download_link_removed, Toast.LENGTH_SHORT).show() },
|
||||||
|
{ throwable ->
|
||||||
|
if (DEBUG) Log.e(TAG, "Failed to remove download link", throwable)
|
||||||
|
showUiErrorSnackbar(this, "Removing download link", throwable)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteDownloadedFile(entity: DownloadedStreamEntity) {
|
||||||
|
val context = requireContext()
|
||||||
|
val uriString = entity.fileUri.takeUnless { it.isBlank() }
|
||||||
|
if (uriString.isNullOrBlank()) {
|
||||||
|
Toast.makeText(context, R.string.download_delete_failed, Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val uri = Uri.parse(uriString)
|
||||||
|
val deleted = when (uri.scheme?.lowercase()) {
|
||||||
|
ContentResolver.SCHEME_CONTENT -> DocumentFile.fromSingleUri(context, uri)?.delete() ?: false
|
||||||
|
ContentResolver.SCHEME_FILE -> uri.path?.let { File(it).delete() } ?: false
|
||||||
|
else -> runCatching { context.contentResolver.delete(uri, null, null) > 0 }.getOrDefault(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deleted) {
|
||||||
|
Toast.makeText(context, R.string.download_delete_failed, Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
removeDownloadAssociation(entity)
|
||||||
|
Toast.makeText(context, R.string.download_deleted, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleRelinkResult(entity: DownloadedStreamEntity, uri: Uri) {
|
||||||
|
val context = requireContext()
|
||||||
|
runCatching {
|
||||||
|
context.contentResolver.takePersistableUriPermission(
|
||||||
|
uri,
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
disposables.add(
|
||||||
|
DownloadedStreamsRepository.relink(context, entity, uri)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{ Toast.makeText(context, R.string.download_relinked, Toast.LENGTH_SHORT).show() },
|
||||||
|
{ throwable ->
|
||||||
|
if (DEBUG) Log.e(TAG, "Failed to relink download", throwable)
|
||||||
|
Toast.makeText(context, R.string.download_relink_failed, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openDownloadsActivity() {
|
||||||
|
val context = requireContext()
|
||||||
|
val intent = Intent(context, DownloadActivity::class.java)
|
||||||
|
runCatching { startActivity(intent) }
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Stream Results
|
// Stream Results
|
||||||
////////////////////////////////////////////////////////////////////////// */
|
////////////////////////////////////////////////////////////////////////// */
|
||||||
|
@ -2270,6 +2569,7 @@ class VideoDetailFragment :
|
||||||
|
|
||||||
private const val MAX_OVERLAY_ALPHA = 0.9f
|
private const val MAX_OVERLAY_ALPHA = 0.9f
|
||||||
private const val MAX_PLAYER_HEIGHT = 0.7f
|
private const val MAX_PLAYER_HEIGHT = 0.7f
|
||||||
|
private val AVAILABILITY_CHECK_INTERVAL_MS = TimeUnit.MINUTES.toMillis(5)
|
||||||
|
|
||||||
const val ACTION_SHOW_MAIN_PLAYER: String =
|
const val ACTION_SHOW_MAIN_PLAYER: String =
|
||||||
App.PACKAGE_NAME + ".VideoDetailFragment.ACTION_SHOW_MAIN_PLAYER"
|
App.PACKAGE_NAME + ".VideoDetailFragment.ACTION_SHOW_MAIN_PLAYER"
|
||||||
|
|
|
@ -134,6 +134,10 @@ public class DownloadMission extends Mission {
|
||||||
*/
|
*/
|
||||||
public MissionRecoveryInfo[] recoveryInfo;
|
public MissionRecoveryInfo[] recoveryInfo;
|
||||||
|
|
||||||
|
public long streamUid = -1;
|
||||||
|
public long downloadedEntityId = -1;
|
||||||
|
public int serviceId = -1;
|
||||||
|
|
||||||
private transient int finishCount;
|
private transient int finishCount;
|
||||||
public transient volatile boolean running;
|
public transient volatile boolean running;
|
||||||
public boolean enqueued;
|
public boolean enqueued;
|
||||||
|
|
|
@ -39,6 +39,8 @@ import androidx.core.content.IntentCompat;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.download.DownloadedStreamsRepository;
|
||||||
|
import org.schabi.newpipe.download.DownloadedStreamsRepository.DownloadAssociation;
|
||||||
import org.schabi.newpipe.download.DownloadActivity;
|
import org.schabi.newpipe.download.DownloadActivity;
|
||||||
import org.schabi.newpipe.player.helper.LockManager;
|
import org.schabi.newpipe.player.helper.LockManager;
|
||||||
import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
|
import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
|
||||||
|
@ -56,6 +58,8 @@ import us.shandian.giga.get.MissionRecoveryInfo;
|
||||||
import us.shandian.giga.postprocessing.Postprocessing;
|
import us.shandian.giga.postprocessing.Postprocessing;
|
||||||
import us.shandian.giga.service.DownloadManager.NetworkState;
|
import us.shandian.giga.service.DownloadManager.NetworkState;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
|
|
||||||
public class DownloadManagerService extends Service {
|
public class DownloadManagerService extends Service {
|
||||||
|
|
||||||
private static final String TAG = "DownloadManagerService";
|
private static final String TAG = "DownloadManagerService";
|
||||||
|
@ -80,6 +84,9 @@ public class DownloadManagerService extends Service {
|
||||||
private static final String EXTRA_PARENT_PATH = "DownloadManagerService.extra.storageParentPath";
|
private static final String EXTRA_PARENT_PATH = "DownloadManagerService.extra.storageParentPath";
|
||||||
private static final String EXTRA_STORAGE_TAG = "DownloadManagerService.extra.storageTag";
|
private static final String EXTRA_STORAGE_TAG = "DownloadManagerService.extra.storageTag";
|
||||||
private static final String EXTRA_RECOVERY_INFO = "DownloadManagerService.extra.recoveryInfo";
|
private static final String EXTRA_RECOVERY_INFO = "DownloadManagerService.extra.recoveryInfo";
|
||||||
|
private static final String EXTRA_STREAM_UID = "DownloadManagerService.extra.streamUid";
|
||||||
|
private static final String EXTRA_DOWNLOADED_ID = "DownloadManagerService.extra.downloadedId";
|
||||||
|
private static final String EXTRA_SERVICE_ID = "DownloadManagerService.extra.serviceId";
|
||||||
|
|
||||||
private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished";
|
private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished";
|
||||||
private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished";
|
private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished";
|
||||||
|
@ -118,6 +125,8 @@ public class DownloadManagerService extends Service {
|
||||||
|
|
||||||
private PendingIntent mOpenDownloadList;
|
private PendingIntent mOpenDownloadList;
|
||||||
|
|
||||||
|
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* notify media scanner on downloaded media file ...
|
* notify media scanner on downloaded media file ...
|
||||||
*
|
*
|
||||||
|
@ -244,6 +253,7 @@ public class DownloadManagerService extends Service {
|
||||||
if (icLauncher != null) icLauncher.recycle();
|
if (icLauncher != null) icLauncher.recycle();
|
||||||
|
|
||||||
mHandler = null;
|
mHandler = null;
|
||||||
|
disposables.clear();
|
||||||
mManager.pauseAllMissions(true);
|
mManager.pauseAllMissions(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -259,6 +269,18 @@ public class DownloadManagerService extends Service {
|
||||||
|
|
||||||
switch (msg.what) {
|
switch (msg.what) {
|
||||||
case MESSAGE_FINISHED:
|
case MESSAGE_FINISHED:
|
||||||
|
if (mission.streamUid >= 0) {
|
||||||
|
DownloadAssociation association =
|
||||||
|
new DownloadAssociation(mission.streamUid, mission.downloadedEntityId);
|
||||||
|
disposables.add(DownloadedStreamsRepository.INSTANCE
|
||||||
|
.markFinished(this, association, mission.serviceId, mission.source,
|
||||||
|
mission.storage, null, null, null, null)
|
||||||
|
.subscribe(
|
||||||
|
() -> { },
|
||||||
|
throwable -> Log.e(TAG,
|
||||||
|
"Failed to update downloaded stream entry", throwable)
|
||||||
|
));
|
||||||
|
}
|
||||||
notifyMediaScanner(mission.storage.getUri());
|
notifyMediaScanner(mission.storage.getUri());
|
||||||
notifyFinishedDownload(mission.storage.getName());
|
notifyFinishedDownload(mission.storage.getName());
|
||||||
mManager.setFinished(mission);
|
mManager.setFinished(mission);
|
||||||
|
@ -361,7 +383,8 @@ public class DownloadManagerService extends Service {
|
||||||
public static void startMission(Context context, String[] urls, StoredFileHelper storage,
|
public static void startMission(Context context, String[] urls, StoredFileHelper storage,
|
||||||
char kind, int threads, String source, String psName,
|
char kind, int threads, String source, String psName,
|
||||||
String[] psArgs, long nearLength,
|
String[] psArgs, long nearLength,
|
||||||
ArrayList<MissionRecoveryInfo> recoveryInfo) {
|
ArrayList<MissionRecoveryInfo> recoveryInfo,
|
||||||
|
long streamUid, long downloadedEntityId, int serviceId) {
|
||||||
final Intent intent = new Intent(context, DownloadManagerService.class)
|
final Intent intent = new Intent(context, DownloadManagerService.class)
|
||||||
.setAction(Intent.ACTION_RUN)
|
.setAction(Intent.ACTION_RUN)
|
||||||
.putExtra(EXTRA_URLS, urls)
|
.putExtra(EXTRA_URLS, urls)
|
||||||
|
@ -374,7 +397,10 @@ public class DownloadManagerService extends Service {
|
||||||
.putExtra(EXTRA_RECOVERY_INFO, recoveryInfo)
|
.putExtra(EXTRA_RECOVERY_INFO, recoveryInfo)
|
||||||
.putExtra(EXTRA_PARENT_PATH, storage.getParentUri())
|
.putExtra(EXTRA_PARENT_PATH, storage.getParentUri())
|
||||||
.putExtra(EXTRA_PATH, storage.getUri())
|
.putExtra(EXTRA_PATH, storage.getUri())
|
||||||
.putExtra(EXTRA_STORAGE_TAG, storage.getTag());
|
.putExtra(EXTRA_STORAGE_TAG, storage.getTag())
|
||||||
|
.putExtra(EXTRA_STREAM_UID, streamUid)
|
||||||
|
.putExtra(EXTRA_DOWNLOADED_ID, downloadedEntityId)
|
||||||
|
.putExtra(EXTRA_SERVICE_ID, serviceId);
|
||||||
|
|
||||||
context.startService(intent);
|
context.startService(intent);
|
||||||
}
|
}
|
||||||
|
@ -390,6 +416,9 @@ public class DownloadManagerService extends Service {
|
||||||
String source = intent.getStringExtra(EXTRA_SOURCE);
|
String source = intent.getStringExtra(EXTRA_SOURCE);
|
||||||
long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0);
|
long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0);
|
||||||
String tag = intent.getStringExtra(EXTRA_STORAGE_TAG);
|
String tag = intent.getStringExtra(EXTRA_STORAGE_TAG);
|
||||||
|
long streamUid = intent.getLongExtra(EXTRA_STREAM_UID, -1L);
|
||||||
|
long downloadedEntityId = intent.getLongExtra(EXTRA_DOWNLOADED_ID, -1L);
|
||||||
|
int serviceId = intent.getIntExtra(EXTRA_SERVICE_ID, -1);
|
||||||
final var recovery = IntentCompat.getParcelableArrayListExtra(intent, EXTRA_RECOVERY_INFO,
|
final var recovery = IntentCompat.getParcelableArrayListExtra(intent, EXTRA_RECOVERY_INFO,
|
||||||
MissionRecoveryInfo.class);
|
MissionRecoveryInfo.class);
|
||||||
Objects.requireNonNull(recovery);
|
Objects.requireNonNull(recovery);
|
||||||
|
@ -412,6 +441,9 @@ public class DownloadManagerService extends Service {
|
||||||
mission.source = source;
|
mission.source = source;
|
||||||
mission.nearLength = nearLength;
|
mission.nearLength = nearLength;
|
||||||
mission.recoveryInfo = recovery.toArray(new MissionRecoveryInfo[0]);
|
mission.recoveryInfo = recovery.toArray(new MissionRecoveryInfo[0]);
|
||||||
|
mission.streamUid = streamUid;
|
||||||
|
mission.downloadedEntityId = downloadedEntityId;
|
||||||
|
mission.serviceId = serviceId;
|
||||||
|
|
||||||
if (ps != null)
|
if (ps != null)
|
||||||
ps.setTemporalDir(DownloadManager.pickAvailableTemporalDir(this));
|
ps.setTemporalDir(DownloadManager.pickAvailableTemporalDir(this));
|
||||||
|
|
76
app/src/main/res/layout/download_status_sheet.xml
Normal file
76
app/src/main/res/layout/download_status_sheet.xml
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="?attr/contrast_background_color"
|
||||||
|
android:paddingStart="24dp"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
android:paddingEnd="24dp"
|
||||||
|
android:paddingBottom="12dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/download_status_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Headline"
|
||||||
|
android:textColor="?android:textColorPrimary"
|
||||||
|
tools:text="Sample Video" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/download_status_subtitle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:paddingBottom="12dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
|
||||||
|
android:textColor="?android:textColorSecondary"
|
||||||
|
tools:text="1080p • 12 Apr 2024" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="?attr/separator_color" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/download_status_open"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selector"
|
||||||
|
android:paddingVertical="16dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
android:textColor="?android:textColorPrimary"
|
||||||
|
tools:text="Open downloaded" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/download_status_delete"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selector"
|
||||||
|
android:paddingVertical="16dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
android:textColor="?android:textColorPrimary"
|
||||||
|
tools:text="Delete file" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/download_status_show_in_folder"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selector"
|
||||||
|
android:paddingVertical="16dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
android:textColor="?android:textColorPrimary"
|
||||||
|
tools:text="Show in folder" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/download_status_remove_link"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selector"
|
||||||
|
android:paddingVertical="16dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
android:textColor="?attr/colorAccent"
|
||||||
|
tools:text="Remove link" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
|
@ -273,8 +273,9 @@
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/detail_primary_control_panel"
|
||||||
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_marginLeft="8dp"
|
android:layout_marginLeft="8dp"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
|
@ -550,10 +551,27 @@
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.chip.Chip
|
||||||
|
android:id="@+id/detail_download_status_chip"
|
||||||
|
style="@style/Widget.MaterialComponents.Chip.Action"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@id/detail_control_panel"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:theme="@style/Theme.MaterialComponents.DayNight.Bridge"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:chipIconVisible="false"
|
||||||
|
app:chipStrokeWidth="0dp"
|
||||||
|
tools:text="Downloaded • 1080p"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:id="@+id/detail_meta_info_separator"
|
android:id="@+id/detail_meta_info_separator"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="1px"
|
android:layout_height="1px"
|
||||||
|
android:layout_below="@id/detail_download_status_chip"
|
||||||
android:layout_marginLeft="8dp"
|
android:layout_marginLeft="8dp"
|
||||||
android:layout_marginRight="8dp"
|
android:layout_marginRight="8dp"
|
||||||
android:background="?attr/separator_color" />
|
android:background="?attr/separator_color" />
|
||||||
|
@ -562,6 +580,7 @@
|
||||||
android:id="@+id/detail_meta_info_text_view"
|
android:id="@+id/detail_meta_info_text_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@id/detail_meta_info_separator"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:padding="12dp"
|
android:padding="12dp"
|
||||||
android:textSize="@dimen/video_item_detail_description_text_size"
|
android:textSize="@dimen/video_item_detail_description_text_size"
|
||||||
|
@ -570,6 +589,7 @@
|
||||||
<View
|
<View
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="1px"
|
android:layout_height="1px"
|
||||||
|
android:layout_below="@id/detail_meta_info_text_view"
|
||||||
android:layout_marginLeft="8dp"
|
android:layout_marginLeft="8dp"
|
||||||
android:layout_marginRight="8dp"
|
android:layout_marginRight="8dp"
|
||||||
android:background="?attr/separator_color" />
|
android:background="?attr/separator_color" />
|
||||||
|
|
|
@ -16,6 +16,21 @@
|
||||||
<string name="share">Share</string>
|
<string name="share">Share</string>
|
||||||
<string name="download">Download</string>
|
<string name="download">Download</string>
|
||||||
<string name="controls_download_desc">Download stream file</string>
|
<string name="controls_download_desc">Download stream file</string>
|
||||||
|
<string name="download_status_downloaded">Downloaded • %1$s</string>
|
||||||
|
<string name="download_status_downloaded_simple">Downloaded</string>
|
||||||
|
<string name="download_status_downloading">Downloading…</string>
|
||||||
|
<string name="download_status_missing">Previously downloaded – file missing</string>
|
||||||
|
<string name="download_action_open">Open file</string>
|
||||||
|
<string name="download_action_show_in_folder">Show in folder</string>
|
||||||
|
<string name="download_action_delete">Delete file</string>
|
||||||
|
<string name="download_action_remove_link">Remove link</string>
|
||||||
|
<string name="download_link_removed">Download link removed</string>
|
||||||
|
<string name="download_relinked">Download relinked</string>
|
||||||
|
<string name="download_open_failed">Unable to open downloaded file</string>
|
||||||
|
<string name="download_folder_open_failed">Unable to open folder</string>
|
||||||
|
<string name="download_relink_failed">Unable to relink file</string>
|
||||||
|
<string name="download_delete_failed">Unable to delete downloaded file</string>
|
||||||
|
<string name="download_deleted">Deleted downloaded file</string>
|
||||||
<string name="search">Search</string>
|
<string name="search">Search</string>
|
||||||
<string name="search_with_service_name">Search %1$s</string>
|
<string name="search_with_service_name">Search %1$s</string>
|
||||||
<string name="search_with_service_name_and_filter">Search %1$s (%2$s)</string>
|
<string name="search_with_service_name_and_filter">Search %1$s (%2$s)</string>
|
||||||
|
|
9
app/src/main/res/values/styles_download.xml
Normal file
9
app/src/main/res/values/styles_download.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<style name="Theme.MaterialComponents.DayNight.BottomSheet" parent="Theme.MaterialComponents.DayNight">
|
||||||
|
<item name="textAppearanceBody1">@style/TextAppearance.MaterialComponents.Body1</item>
|
||||||
|
<item name="textAppearanceBody2">@style/TextAppearance.MaterialComponents.Body2</item>
|
||||||
|
<item name="textAppearanceHeadline6">@style/TextAppearance.MaterialComponents.Headline6</item>
|
||||||
|
<item name="textAppearanceSubtitle1">@style/TextAppearance.MaterialComponents.Body2</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
Loading…
Add table
Add a link
Reference in a new issue