mirror of
https://github.com/TeamNewPipe/NewPipe.git
synced 2025-10-03 09:49:21 +02:00
Refactor download status persistence and UI
Restore finshed mission conditoina
This commit is contained in:
parent
b5cb367edb
commit
13b10b6e52
28 changed files with 682 additions and 1852 deletions
|
@ -1,856 +0,0 @@
|
|||
{
|
||||
"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,7 +25,6 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
|||
import org.acra.ACRA.init
|
||||
import org.acra.ACRA.isACRASenderServiceProcess
|
||||
import org.acra.config.CoreConfigurationBuilder
|
||||
import org.schabi.newpipe.download.DownloadMaintenance
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader
|
||||
|
@ -121,8 +120,6 @@ open class App :
|
|||
configureRxJavaErrorHandler()
|
||||
|
||||
YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl)
|
||||
|
||||
DownloadMaintenance.schedule(this)
|
||||
}
|
||||
|
||||
override fun newImageLoader(context: Context): ImageLoader =
|
||||
|
|
|
@ -9,7 +9,6 @@ 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_7_8;
|
||||
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.database.Cursor;
|
||||
|
@ -30,7 +29,7 @@ public final class NewPipeDatabase {
|
|||
return Room
|
||||
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
|
||||
.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_9_10)
|
||||
MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
package org.schabi.newpipe.database;
|
||||
|
||||
import static org.schabi.newpipe.database.Migrations.DB_VER_10;
|
||||
import static org.schabi.newpipe.database.Migrations.DB_VER_9;
|
||||
|
||||
import androidx.room.Database;
|
||||
import androidx.room.RoomDatabase;
|
||||
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.FeedGroupDAO;
|
||||
import org.schabi.newpipe.database.feed.model.FeedEntity;
|
||||
|
@ -38,9 +36,9 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
|||
StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class,
|
||||
PlaylistEntity.class, PlaylistStreamEntity.class, PlaylistRemoteEntity.class,
|
||||
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
|
||||
FeedLastUpdatedEntity.class, DownloadedStreamEntity.class
|
||||
FeedLastUpdatedEntity.class
|
||||
},
|
||||
version = DB_VER_10
|
||||
version = DB_VER_9
|
||||
)
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
public static final String DATABASE_NAME = "newpipe.db";
|
||||
|
@ -64,6 +62,4 @@ public abstract class AppDatabase extends RoomDatabase {
|
|||
public abstract FeedGroupDAO feedGroupDAO();
|
||||
|
||||
public abstract SubscriptionDAO subscriptionDAO();
|
||||
|
||||
public abstract DownloadedStreamsDao downloadedStreamsDao();
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package org.schabi.newpipe.database
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import org.schabi.newpipe.database.download.DownloadedStreamStatus
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
||||
import java.time.Instant
|
||||
|
@ -50,14 +49,4 @@ class Converters {
|
|||
fun feedGroupIconOf(id: Int): FeedGroupIcon {
|
||||
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,7 +27,6 @@ public final class Migrations {
|
|||
public static final int DB_VER_7 = 7;
|
||||
public static final int DB_VER_8 = 8;
|
||||
public static final int DB_VER_9 = 9;
|
||||
public static final int DB_VER_10 = 10;
|
||||
|
||||
private static final String TAG = Migrations.class.getName();
|
||||
public static final boolean DEBUG = MainActivity.DEBUG;
|
||||
|
@ -303,22 +302,6 @@ 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() {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,103 +0,0 @@
|
|||
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"
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
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,14 +81,6 @@ public class DownloadActivity extends AppCompatActivity {
|
|||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
new Thread(() ->
|
||||
DownloadMaintenance.revalidateAvailable(DownloadActivity.this, 10)
|
||||
).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
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,9 +79,7 @@ import java.util.List;
|
|||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import us.shandian.giga.get.MissionRecoveryInfo;
|
||||
import us.shandian.giga.postprocessing.Postprocessing;
|
||||
|
@ -1135,50 +1133,27 @@ public class DownloadDialog extends DialogFragment
|
|||
}
|
||||
|
||||
final String qualityLabel = buildQualityLabel(selectedStream);
|
||||
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;
|
||||
|
||||
final char missionKind = kind;
|
||||
final int missionThreads = threads;
|
||||
final String missionSourceUrl = currentInfo.getUrl();
|
||||
final String missionPsName = psName;
|
||||
final String[] missionPsArgs = psArgs;
|
||||
final long missionNearLength = nearLength;
|
||||
|
||||
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,
|
||||
kind,
|
||||
threads,
|
||||
currentInfo.getUrl(),
|
||||
psName,
|
||||
psArgs,
|
||||
nearLength,
|
||||
new ArrayList<>(recoveryInfo),
|
||||
association.getStreamUid(),
|
||||
association.getEntityId(),
|
||||
currentInfo.getServiceId()
|
||||
-1L,
|
||||
currentInfo.getServiceId(),
|
||||
qualityLabel
|
||||
);
|
||||
|
||||
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
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
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,207 @@
|
|||
package org.schabi.newpipe.download
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Message
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import us.shandian.giga.get.DownloadMission
|
||||
import us.shandian.giga.get.FinishedMission
|
||||
import us.shandian.giga.service.DownloadManager
|
||||
import us.shandian.giga.service.DownloadManagerService
|
||||
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder
|
||||
import us.shandian.giga.service.MissionState
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
sealed interface DownloadStatus {
|
||||
data object None : DownloadStatus
|
||||
data class InProgress(val running: Boolean) : DownloadStatus
|
||||
data class Completed(val info: CompletedDownload) : DownloadStatus
|
||||
}
|
||||
|
||||
data class CompletedDownload(
|
||||
val displayName: String?,
|
||||
val qualityLabel: String?,
|
||||
val mimeType: String?,
|
||||
val fileUri: Uri?,
|
||||
val parentUri: Uri?,
|
||||
val fileAvailable: Boolean
|
||||
)
|
||||
|
||||
object DownloadStatusRepository {
|
||||
|
||||
fun observe(context: Context, serviceId: Int, url: String): Flow<DownloadStatus> = callbackFlow {
|
||||
if (serviceId < 0 || url.isBlank()) {
|
||||
trySend(DownloadStatus.None)
|
||||
close()
|
||||
return@callbackFlow
|
||||
}
|
||||
|
||||
val appContext = context.applicationContext
|
||||
val intent = Intent(appContext, DownloadManagerService::class.java)
|
||||
appContext.startService(intent)
|
||||
var binder: DownloadManagerBinder? = null
|
||||
var registeredCallback: Handler.Callback? = null
|
||||
|
||||
val connection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
val downloadBinder = service as? DownloadManagerBinder
|
||||
if (downloadBinder == null) {
|
||||
trySend(DownloadStatus.None)
|
||||
appContext.unbindService(this)
|
||||
close()
|
||||
return
|
||||
}
|
||||
binder = downloadBinder
|
||||
trySend(downloadBinder.getDownloadStatus(serviceId, url, false).toDownloadStatus())
|
||||
|
||||
val callback = Handler.Callback { message: Message ->
|
||||
val mission = message.obj
|
||||
if (mission.matches(serviceId, url)) {
|
||||
val snapshot = downloadBinder.getDownloadStatus(serviceId, url, false)
|
||||
trySend(snapshot.toDownloadStatus())
|
||||
}
|
||||
false
|
||||
}
|
||||
registeredCallback = callback
|
||||
downloadBinder.addMissionEventListener(callback)
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
registeredCallback?.let { callback -> binder?.removeMissionEventListener(callback) }
|
||||
binder = null
|
||||
trySend(DownloadStatus.None)
|
||||
}
|
||||
}
|
||||
|
||||
val bound = appContext.bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||
if (!bound) {
|
||||
trySend(DownloadStatus.None)
|
||||
close()
|
||||
return@callbackFlow
|
||||
}
|
||||
|
||||
awaitClose {
|
||||
registeredCallback?.let { callback -> binder?.removeMissionEventListener(callback) }
|
||||
runCatching { appContext.unbindService(connection) }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun refresh(context: Context, serviceId: Int, url: String): DownloadStatus {
|
||||
if (serviceId < 0 || url.isBlank()) return DownloadStatus.None
|
||||
return withBinder(context) { binder ->
|
||||
binder.getDownloadStatus(serviceId, url, true).toDownloadStatus()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteFile(context: Context, serviceId: Int, url: String): Boolean {
|
||||
if (serviceId < 0 || url.isBlank()) return false
|
||||
return withBinder(context) { binder ->
|
||||
binder.deleteFinishedMission(serviceId, url, true)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removeLink(context: Context, serviceId: Int, url: String): Boolean {
|
||||
if (serviceId < 0 || url.isBlank()) return false
|
||||
return withBinder(context) { binder ->
|
||||
binder.deleteFinishedMission(serviceId, url, false)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T> withBinder(context: Context, block: (DownloadManagerBinder) -> T): T {
|
||||
val appContext = context.applicationContext
|
||||
val intent = Intent(appContext, DownloadManagerService::class.java)
|
||||
appContext.startService(intent)
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
val connection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
val binder = service as? DownloadManagerBinder
|
||||
if (binder == null) {
|
||||
if (continuation.isActive) {
|
||||
continuation.resumeWithException(IllegalStateException("Download service binder is null"))
|
||||
}
|
||||
appContext.unbindService(this)
|
||||
return
|
||||
}
|
||||
try {
|
||||
val result = block(binder)
|
||||
if (continuation.isActive) {
|
||||
continuation.resume(result)
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
if (continuation.isActive) {
|
||||
continuation.resumeWithException(throwable)
|
||||
}
|
||||
} finally {
|
||||
appContext.unbindService(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
if (continuation.isActive) {
|
||||
continuation.resumeWithException(IllegalStateException("Download service disconnected"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val bound = appContext.bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||
if (!bound) {
|
||||
continuation.resumeWithException(IllegalStateException("Unable to bind download service"))
|
||||
return@suspendCancellableCoroutine
|
||||
}
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
runCatching { appContext.unbindService(connection) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Any?.matches(serviceId: Int, url: String): Boolean {
|
||||
return when (this) {
|
||||
is DownloadMission -> this.serviceId == serviceId && url == this.source
|
||||
is FinishedMission -> this.serviceId == serviceId && url == this.source
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@MainThread
|
||||
internal fun DownloadManager.DownloadStatusSnapshot?.toDownloadStatus(): DownloadStatus {
|
||||
if (this == null || state == MissionState.None) {
|
||||
return DownloadStatus.None
|
||||
}
|
||||
return when (state) {
|
||||
MissionState.Pending, MissionState.PendingRunning ->
|
||||
DownloadStatus.InProgress(state == MissionState.PendingRunning)
|
||||
MissionState.Finished -> {
|
||||
val mission = finishedMission
|
||||
if (mission == null) {
|
||||
DownloadStatus.None
|
||||
} else {
|
||||
val storage = mission.storage
|
||||
val hasStorage = storage != null && !storage.isInvalid()
|
||||
val info = CompletedDownload(
|
||||
displayName = storage?.getName(),
|
||||
qualityLabel = mission.qualityLabel,
|
||||
mimeType = if (hasStorage) storage!!.getType() else null,
|
||||
fileUri = if (hasStorage) storage!!.getUri() else null,
|
||||
parentUri = if (hasStorage) storage!!.getParentUri() else null,
|
||||
fileAvailable = fileExists && hasStorage
|
||||
)
|
||||
DownloadStatus.Completed(info)
|
||||
}
|
||||
}
|
||||
else -> DownloadStatus.None
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,238 +0,0 @@
|
|||
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()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
package org.schabi.newpipe.download.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.AssistChip
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.download.CompletedDownload
|
||||
import org.schabi.newpipe.fragments.detail.DownloadChipState
|
||||
import org.schabi.newpipe.fragments.detail.DownloadUiState
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DownloadStatusHost(
|
||||
state: DownloadUiState,
|
||||
onChipClick: () -> Unit,
|
||||
onDismissSheet: () -> Unit,
|
||||
onOpenFile: (CompletedDownload) -> Unit,
|
||||
onDeleteFile: (CompletedDownload) -> Unit,
|
||||
onRemoveLink: (CompletedDownload) -> Unit,
|
||||
onShowInFolder: (CompletedDownload) -> Unit
|
||||
) {
|
||||
val chipState = state.chipState
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
|
||||
if (state.isSheetVisible && chipState is DownloadChipState.Downloaded) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismissSheet,
|
||||
sheetState = sheetState
|
||||
) {
|
||||
DownloadSheetContent(
|
||||
info = chipState.info,
|
||||
onOpenFile = { onOpenFile(chipState.info) },
|
||||
onDeleteFile = { onDeleteFile(chipState.info) },
|
||||
onRemoveLink = { onRemoveLink(chipState.info) },
|
||||
onShowInFolder = { onShowInFolder(chipState.info) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
when (chipState) {
|
||||
DownloadChipState.Hidden -> Unit
|
||||
DownloadChipState.Downloading -> AssistChip(
|
||||
onClick = onChipClick,
|
||||
label = { Text(text = stringResource(id = R.string.download_status_downloading)) }
|
||||
)
|
||||
is DownloadChipState.Downloaded -> {
|
||||
val label = chipState.info.qualityLabel
|
||||
val text = if (!label.isNullOrBlank()) {
|
||||
stringResource(R.string.download_status_downloaded, label)
|
||||
} else {
|
||||
stringResource(R.string.download_status_downloaded_simple)
|
||||
}
|
||||
AssistChip(
|
||||
onClick = onChipClick,
|
||||
label = { Text(text = text) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DownloadSheetContent(
|
||||
info: CompletedDownload,
|
||||
onOpenFile: () -> Unit,
|
||||
onDeleteFile: () -> Unit,
|
||||
onRemoveLink: () -> Unit,
|
||||
onShowInFolder: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp, vertical = 16.dp)
|
||||
) {
|
||||
val title = info.displayName ?: stringResource(id = R.string.download)
|
||||
Text(text = title, style = MaterialTheme.typography.titleLarge)
|
||||
|
||||
val subtitleParts = buildList {
|
||||
info.qualityLabel?.takeIf { it.isNotBlank() }?.let { add(it) }
|
||||
if (!info.fileAvailable) {
|
||||
add(stringResource(id = R.string.download_status_missing))
|
||||
}
|
||||
}
|
||||
if (subtitleParts.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Text(
|
||||
text = subtitleParts.joinToString(" • "),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
val showFileActions = info.fileAvailable && info.fileUri != null
|
||||
if (showFileActions) {
|
||||
TextButton(onClick = onOpenFile) {
|
||||
Text(text = stringResource(id = R.string.download_action_open))
|
||||
}
|
||||
TextButton(onClick = onShowInFolder, enabled = info.parentUri != null) {
|
||||
Text(text = stringResource(id = R.string.download_action_show_in_folder))
|
||||
}
|
||||
TextButton(onClick = onDeleteFile) {
|
||||
Text(text = stringResource(id = R.string.download_action_delete))
|
||||
}
|
||||
}
|
||||
|
||||
TextButton(onClick = onRemoveLink) {
|
||||
Text(text = stringResource(id = R.string.download_action_remove_link), color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
|
@ -4,7 +4,6 @@ import android.animation.ValueAnimator
|
|||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
|
@ -13,7 +12,6 @@ import android.content.pm.ActivityInfo
|
|||
import android.database.ContentObserver
|
||||
import android.graphics.Color
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
|
@ -23,7 +21,6 @@ import android.provider.Settings
|
|||
import android.util.DisplayMetrics
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.ContextThemeWrapper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
|
@ -35,14 +32,14 @@ import android.view.WindowManager
|
|||
import android.view.animation.DecelerateInterpolator
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.edit
|
||||
|
@ -50,7 +47,9 @@ import androidx.core.net.toUri
|
|||
import androidx.core.os.postDelayed
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import coil3.util.CoilUtils
|
||||
import com.evernote.android.state.State
|
||||
|
@ -59,22 +58,18 @@ import com.google.android.exoplayer2.PlaybackParameters
|
|||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
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.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.schabi.newpipe.App
|
||||
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.databinding.DownloadStatusSheetBinding
|
||||
import org.schabi.newpipe.databinding.FragmentVideoDetailBinding
|
||||
import org.schabi.newpipe.download.DownloadActivity
|
||||
import org.schabi.newpipe.download.DownloadAvailabilityChecker
|
||||
import org.schabi.newpipe.download.CompletedDownload
|
||||
import org.schabi.newpipe.download.DownloadDialog
|
||||
import org.schabi.newpipe.download.DownloadedStreamsRepository
|
||||
import org.schabi.newpipe.download.ui.DownloadStatusHost
|
||||
import org.schabi.newpipe.error.ErrorInfo
|
||||
import org.schabi.newpipe.error.ErrorUtil.Companion.showSnackbar
|
||||
import org.schabi.newpipe.error.ErrorUtil.Companion.showUiErrorSnackbar
|
||||
|
@ -113,6 +108,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueue
|
|||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue
|
||||
import org.schabi.newpipe.player.ui.MainPlayerUi
|
||||
import org.schabi.newpipe.player.ui.VideoPlayerUi
|
||||
import org.schabi.newpipe.ui.theme.AppTheme
|
||||
import org.schabi.newpipe.util.DependentPreferenceHelper
|
||||
import org.schabi.newpipe.util.DeviceUtils
|
||||
import org.schabi.newpipe.util.ExtractorHelper
|
||||
|
@ -129,7 +125,6 @@ import org.schabi.newpipe.util.ThemeHelper
|
|||
import org.schabi.newpipe.util.external_communication.KoreUtils
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||
import org.schabi.newpipe.util.image.CoilHelper
|
||||
import java.io.File
|
||||
import java.util.LinkedList
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.abs
|
||||
|
@ -158,6 +153,7 @@ class VideoDetailFragment :
|
|||
// can't make this lateinit because it needs to be set to null when the view is destroyed
|
||||
private var nullableBinding: FragmentVideoDetailBinding? = null
|
||||
private val binding: FragmentVideoDetailBinding get() = nullableBinding!!
|
||||
private val downloadStatusViewModel: VideoDownloadStatusViewModel by viewModels()
|
||||
private lateinit var pageAdapter: TabAdapter
|
||||
private var settingsContentObserver: ContentObserver? = null
|
||||
|
||||
|
@ -196,17 +192,6 @@ class VideoDetailFragment :
|
|||
private var currentWorker: Disposable? = null
|
||||
private val disposables = CompositeDisposable()
|
||||
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
|
||||
|
@ -295,6 +280,26 @@ class VideoDetailFragment :
|
|||
): View {
|
||||
val newBinding = FragmentVideoDetailBinding.inflate(inflater, container, false)
|
||||
nullableBinding = newBinding
|
||||
newBinding.detailDownloadStatusCompose?.apply {
|
||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||
setContent {
|
||||
AppTheme {
|
||||
val uiState = downloadStatusViewModel.uiState.collectAsStateWithLifecycle().value
|
||||
val composeContext = LocalContext.current
|
||||
DownloadStatusHost(
|
||||
state = uiState,
|
||||
onChipClick = {
|
||||
downloadStatusViewModel.onChipClicked(composeContext.applicationContext)
|
||||
},
|
||||
onDismissSheet = { downloadStatusViewModel.dismissSheet() },
|
||||
onOpenFile = { info -> openDownloaded(info) },
|
||||
onDeleteFile = { info -> deleteDownloadedFile(info) },
|
||||
onRemoveLink = { info -> removeDownloadLink(info) },
|
||||
onShowInFolder = { info -> showDownloadedInFolder(info) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return newBinding.getRoot()
|
||||
}
|
||||
|
||||
|
@ -374,13 +379,6 @@ class VideoDetailFragment :
|
|||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
downloadStatusDisposable?.let {
|
||||
disposables.remove(it)
|
||||
it.dispose()
|
||||
}
|
||||
downloadStatusDisposable = null
|
||||
currentDownloadedStream = null
|
||||
currentStreamUid = null
|
||||
nullableBinding = null
|
||||
}
|
||||
|
||||
|
@ -1399,8 +1397,8 @@ class VideoDetailFragment :
|
|||
currentInfo = info
|
||||
setInitialData(info.serviceId, info.originalUrl, info.name, playQueue)
|
||||
|
||||
updateDownloadChip(null)
|
||||
observeDownloadStatus(info)
|
||||
downloadStatusViewModel.dismissSheet()
|
||||
downloadStatusViewModel.setStream(requireContext().applicationContext, info.serviceId, info.url)
|
||||
|
||||
updateTabs(info)
|
||||
|
||||
|
@ -1580,165 +1578,15 @@ 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)
|
||||
private fun openDownloaded(info: CompletedDownload) {
|
||||
val uri = info.fileUri
|
||||
if (uri == null) {
|
||||
Toast.makeText(requireContext(), R.string.download_open_failed, Toast.LENGTH_SHORT).show()
|
||||
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 ?: "*/*")
|
||||
setDataAndType(uri, info.mimeType ?: "*/*")
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
|
||||
|
@ -1749,98 +1597,61 @@ class VideoDetailFragment :
|
|||
}
|
||||
}
|
||||
|
||||
private fun showInFolder(entity: DownloadedStreamEntity) {
|
||||
val parent = entity.parentUri?.takeIf { it.isNotBlank() }?.let(Uri::parse)
|
||||
private fun showDownloadedInFolder(info: CompletedDownload) {
|
||||
val parent = info.parentUri
|
||||
if (parent == null) {
|
||||
Toast.makeText(requireContext(), R.string.download_folder_open_failed, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
val context = requireContext()
|
||||
val viewIntent = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(parent, DocumentsContract.Document.MIME_TYPE_DIR)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
|
||||
runCatching { startActivity(intent) }
|
||||
runCatching { startActivity(viewIntent) }
|
||||
.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()
|
||||
Toast.makeText(context, 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()
|
||||
private fun deleteDownloadedFile(info: CompletedDownload) {
|
||||
if (!info.fileAvailable) {
|
||||
Toast.makeText(requireContext(), 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)
|
||||
val appContext = requireContext().applicationContext
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
val success = downloadStatusViewModel.deleteFile(appContext)
|
||||
val message = if (success) {
|
||||
R.string.download_deleted
|
||||
} else {
|
||||
R.string.download_delete_failed
|
||||
}
|
||||
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
if (!deleted) {
|
||||
Toast.makeText(context, R.string.download_delete_failed, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
private fun removeDownloadLink(@Suppress("UNUSED_PARAMETER") info: CompletedDownload) {
|
||||
val appContext = requireContext().applicationContext
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
val success = downloadStatusViewModel.removeLink(appContext)
|
||||
val message = if (success) {
|
||||
R.string.download_link_removed
|
||||
} else {
|
||||
R.string.download_delete_failed
|
||||
}
|
||||
|
||||
removeDownloadAssociation(entity)
|
||||
Toast.makeText(context, R.string.download_deleted, Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(requireContext(), message, 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) }
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
package org.schabi.newpipe.fragments.detail
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.schabi.newpipe.download.CompletedDownload
|
||||
import org.schabi.newpipe.download.DownloadStatus
|
||||
import org.schabi.newpipe.download.DownloadStatusRepository
|
||||
|
||||
class VideoDownloadStatusViewModel : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(DownloadUiState())
|
||||
val uiState: StateFlow<DownloadUiState> = _uiState
|
||||
|
||||
private var observeJob: Job? = null
|
||||
private var currentServiceId: Int = -1
|
||||
private var currentUrl: String? = null
|
||||
|
||||
fun setStream(context: Context, serviceId: Int, url: String?) {
|
||||
val normalizedUrl = url ?: ""
|
||||
if (serviceId < 0 || normalizedUrl.isBlank()) {
|
||||
observeJob?.cancel()
|
||||
observeJob = null
|
||||
currentServiceId = -1
|
||||
currentUrl = null
|
||||
_uiState.value = DownloadUiState()
|
||||
return
|
||||
}
|
||||
|
||||
if (currentServiceId == serviceId && currentUrl == normalizedUrl) {
|
||||
return
|
||||
}
|
||||
|
||||
currentServiceId = serviceId
|
||||
currentUrl = normalizedUrl
|
||||
|
||||
val appContext = context.applicationContext
|
||||
|
||||
observeJob?.cancel()
|
||||
observeJob = viewModelScope.launch {
|
||||
DownloadStatusRepository.observe(appContext, serviceId, normalizedUrl).collectLatest { status ->
|
||||
_uiState.update { it.copy(chipState = status.toChipState()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onChipClicked(context: Context) {
|
||||
val url = currentUrl ?: return
|
||||
val serviceId = currentServiceId
|
||||
viewModelScope.launch {
|
||||
val result = runCatching {
|
||||
DownloadStatusRepository.refresh(context.applicationContext, serviceId, url)
|
||||
}
|
||||
result.getOrNull()?.let { status ->
|
||||
_uiState.update {
|
||||
val chipState = status.toChipState()
|
||||
it.copy(
|
||||
chipState = chipState,
|
||||
isSheetVisible = chipState is DownloadChipState.Downloaded
|
||||
)
|
||||
}
|
||||
}
|
||||
if (result.isFailure) {
|
||||
_uiState.update { it.copy(isSheetVisible = false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun dismissSheet() {
|
||||
_uiState.update { it.copy(isSheetVisible = false) }
|
||||
}
|
||||
|
||||
suspend fun deleteFile(context: Context): Boolean {
|
||||
val url = currentUrl ?: return false
|
||||
val serviceId = currentServiceId
|
||||
val success = runCatching {
|
||||
DownloadStatusRepository.deleteFile(context.applicationContext, serviceId, url)
|
||||
}.getOrDefault(false)
|
||||
if (success) {
|
||||
_uiState.update { it.copy(isSheetVisible = false) }
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
suspend fun removeLink(context: Context): Boolean {
|
||||
val url = currentUrl ?: return false
|
||||
val serviceId = currentServiceId
|
||||
val success = runCatching {
|
||||
DownloadStatusRepository.removeLink(context.applicationContext, serviceId, url)
|
||||
}.getOrDefault(false)
|
||||
if (success) {
|
||||
_uiState.update { it.copy(isSheetVisible = false) }
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
private fun DownloadStatus.toChipState(): DownloadChipState = when (this) {
|
||||
DownloadStatus.None -> DownloadChipState.Hidden
|
||||
is DownloadStatus.InProgress -> DownloadChipState.Downloading
|
||||
is DownloadStatus.Completed -> DownloadChipState.Downloaded(info)
|
||||
}
|
||||
}
|
||||
|
||||
data class DownloadUiState(
|
||||
val chipState: DownloadChipState = DownloadChipState.Hidden,
|
||||
val isSheetVisible: Boolean = false
|
||||
)
|
||||
|
||||
sealed interface DownloadChipState {
|
||||
data object Hidden : DownloadChipState
|
||||
data object Downloading : DownloadChipState
|
||||
data class Downloaded(val info: CompletedDownload) : DownloadChipState
|
||||
}
|
|
@ -135,8 +135,8 @@ public class DownloadMission extends Mission {
|
|||
public MissionRecoveryInfo[] recoveryInfo;
|
||||
|
||||
public long streamUid = -1;
|
||||
public long downloadedEntityId = -1;
|
||||
public int serviceId = -1;
|
||||
public String qualityLabel = null;
|
||||
|
||||
private transient int finishCount;
|
||||
public transient volatile boolean running;
|
||||
|
|
|
@ -4,6 +4,10 @@ import androidx.annotation.NonNull;
|
|||
|
||||
public class FinishedMission extends Mission {
|
||||
|
||||
public int serviceId = -1;
|
||||
public long streamUid = -1;
|
||||
public String qualityLabel = null;
|
||||
|
||||
public FinishedMission() {
|
||||
}
|
||||
|
||||
|
@ -13,6 +17,9 @@ public class FinishedMission extends Mission {
|
|||
timestamp = mission.timestamp;
|
||||
kind = mission.kind;
|
||||
storage = mission.storage;
|
||||
serviceId = mission.serviceId;
|
||||
streamUid = mission.streamUid;
|
||||
qualityLabel = mission.qualityLabel;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
|
|||
// TODO: use NewPipeSQLiteHelper ('s constants) when playlist branch is merged (?)
|
||||
private static final String DATABASE_NAME = "downloads.db";
|
||||
|
||||
private static final int DATABASE_VERSION = 4;
|
||||
private static final int DATABASE_VERSION = 5;
|
||||
|
||||
/**
|
||||
* The table name of download missions (old)
|
||||
|
@ -56,6 +56,12 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
|
|||
|
||||
private static final String KEY_PATH = "path";
|
||||
|
||||
private static final String KEY_SERVICE_ID = "service_id";
|
||||
|
||||
private static final String KEY_STREAM_UID = "stream_uid";
|
||||
|
||||
private static final String KEY_QUALITY_LABEL = "quality_label";
|
||||
|
||||
/**
|
||||
* The statement to create the table
|
||||
*/
|
||||
|
@ -66,6 +72,9 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
|
|||
KEY_DONE + " INTEGER NOT NULL, " +
|
||||
KEY_TIMESTAMP + " INTEGER NOT NULL, " +
|
||||
KEY_KIND + " TEXT NOT NULL, " +
|
||||
KEY_SERVICE_ID + " INTEGER NOT NULL DEFAULT -1, " +
|
||||
KEY_STREAM_UID + " INTEGER NOT NULL DEFAULT -1, " +
|
||||
KEY_QUALITY_LABEL + " TEXT, " +
|
||||
" UNIQUE(" + KEY_TIMESTAMP + ", " + KEY_PATH + "));";
|
||||
|
||||
|
||||
|
@ -121,6 +130,17 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
|
|||
|
||||
cursor.close();
|
||||
db.execSQL("DROP TABLE " + MISSIONS_TABLE_NAME_v2);
|
||||
oldVersion++;
|
||||
}
|
||||
|
||||
if (oldVersion == 4) {
|
||||
db.execSQL("ALTER TABLE " + FINISHED_TABLE_NAME + " ADD COLUMN "
|
||||
+ KEY_SERVICE_ID + " INTEGER NOT NULL DEFAULT -1");
|
||||
db.execSQL("ALTER TABLE " + FINISHED_TABLE_NAME + " ADD COLUMN "
|
||||
+ KEY_STREAM_UID + " INTEGER NOT NULL DEFAULT -1");
|
||||
db.execSQL("ALTER TABLE " + FINISHED_TABLE_NAME + " ADD COLUMN "
|
||||
+ KEY_QUALITY_LABEL + " TEXT");
|
||||
oldVersion++;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -137,6 +157,17 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
|
|||
values.put(KEY_DONE, downloadMission.length);
|
||||
values.put(KEY_TIMESTAMP, downloadMission.timestamp);
|
||||
values.put(KEY_KIND, String.valueOf(downloadMission.kind));
|
||||
if (downloadMission instanceof DownloadMission) {
|
||||
DownloadMission dm = (DownloadMission) downloadMission;
|
||||
values.put(KEY_SERVICE_ID, dm.serviceId);
|
||||
values.put(KEY_STREAM_UID, dm.streamUid);
|
||||
values.put(KEY_QUALITY_LABEL, dm.qualityLabel);
|
||||
} else if (downloadMission instanceof FinishedMission) {
|
||||
FinishedMission fm = (FinishedMission) downloadMission;
|
||||
values.put(KEY_SERVICE_ID, fm.serviceId);
|
||||
values.put(KEY_STREAM_UID, fm.streamUid);
|
||||
values.put(KEY_QUALITY_LABEL, fm.qualityLabel);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
|
@ -152,6 +183,9 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
|
|||
mission.length = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE));
|
||||
mission.timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP));
|
||||
mission.kind = kind.charAt(0);
|
||||
mission.serviceId = cursor.getInt(cursor.getColumnIndexOrThrow(KEY_SERVICE_ID));
|
||||
mission.streamUid = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_STREAM_UID));
|
||||
mission.qualityLabel = cursor.getString(cursor.getColumnIndexOrThrow(KEY_QUALITY_LABEL));
|
||||
|
||||
try {
|
||||
mission.storage = new StoredFileHelper(context,null, Uri.parse(path), "");
|
||||
|
@ -200,11 +234,10 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
|
|||
database.delete(FINISHED_TABLE_NAME, KEY_TIMESTAMP + " = ?", new String[]{ts});
|
||||
} else {
|
||||
database.delete(FINISHED_TABLE_NAME, KEY_TIMESTAMP + " = ? AND " + KEY_PATH + " = ?", new String[]{
|
||||
ts, mission.storage.getUri().toString()
|
||||
});
|
||||
ts, mission.storage.getUri().toString()});
|
||||
}
|
||||
} else {
|
||||
throw new UnsupportedOperationException("DownloadMission");
|
||||
database.delete(FINISHED_TABLE_NAME, KEY_TIMESTAMP + " = ?", new String[]{ts});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -217,11 +250,11 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
|
|||
|
||||
if (mission instanceof FinishedMission) {
|
||||
if (mission.storage.isInvalid()) {
|
||||
rowsAffected = database.update(FINISHED_TABLE_NAME, values, KEY_TIMESTAMP + " = ?", new String[]{ts});
|
||||
rowsAffected = database.update(FINISHED_TABLE_NAME, values, KEY_TIMESTAMP + " = ?",
|
||||
new String[]{ts});
|
||||
} else {
|
||||
rowsAffected = database.update(FINISHED_TABLE_NAME, values, KEY_PATH + " = ?", new String[]{
|
||||
mission.storage.getUri().toString()
|
||||
});
|
||||
rowsAffected = database.update(FINISHED_TABLE_NAME, values, KEY_PATH + " = ?",
|
||||
new String[]{mission.storage.getUri().toString()});
|
||||
}
|
||||
} else {
|
||||
throw new UnsupportedOperationException("DownloadMission");
|
||||
|
|
|
@ -14,6 +14,7 @@ import java.util.ArrayList;
|
|||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
import us.shandian.giga.get.FinishedMission;
|
||||
|
@ -333,6 +334,16 @@ public class DownloadManager {
|
|||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private DownloadMission getPendingMission(int serviceId, String url) {
|
||||
for (DownloadMission mission : mMissionsPending) {
|
||||
if (mission.serviceId == serviceId && Objects.equals(mission.source, url)) {
|
||||
return mission;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the index into {@link #mMissionsFinished} of a finished mission by its path, return
|
||||
* {@code -1} if there is no such mission. This function also checks if the matched mission's
|
||||
|
@ -342,6 +353,26 @@ public class DownloadManager {
|
|||
* @param storage where the file would be stored
|
||||
* @return the mission index or -1 if no such mission exists
|
||||
*/
|
||||
@Nullable
|
||||
private FinishedMission getFinishedMission(int serviceId, String url) {
|
||||
for (FinishedMission mission : mMissionsFinished) {
|
||||
if (mission.serviceId == serviceId && Objects.equals(mission.source, url)) {
|
||||
return mission;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isFileAvailable(@NonNull FinishedMission mission) {
|
||||
if (mission.storage == null || mission.storage.isInvalid()) {
|
||||
return false;
|
||||
}
|
||||
if (!mission.storage.existsAsFile()) {
|
||||
return false;
|
||||
}
|
||||
return mission.storage.length() > 0;
|
||||
}
|
||||
|
||||
private int getFinishedMissionIndex(StoredFileHelper storage) {
|
||||
for (int i = 0; i < mMissionsFinished.size(); i++) {
|
||||
if (mMissionsFinished.get(i).storage.equals(storage)) {
|
||||
|
@ -427,6 +458,50 @@ public class DownloadManager {
|
|||
}
|
||||
}
|
||||
|
||||
public static final class DownloadStatusSnapshot {
|
||||
public final MissionState state;
|
||||
public final DownloadMission pendingMission;
|
||||
public final FinishedMission finishedMission;
|
||||
public final boolean fileExists;
|
||||
|
||||
DownloadStatusSnapshot(MissionState state, DownloadMission pendingMission,
|
||||
FinishedMission finishedMission, boolean fileExists) {
|
||||
this.state = state;
|
||||
this.pendingMission = pendingMission;
|
||||
this.finishedMission = finishedMission;
|
||||
this.fileExists = fileExists;
|
||||
}
|
||||
}
|
||||
|
||||
DownloadStatusSnapshot getDownloadStatus(int serviceId, String url, boolean revalidateFile) {
|
||||
synchronized (this) {
|
||||
DownloadMission pending = getPendingMission(serviceId, url);
|
||||
if (pending != null) {
|
||||
MissionState state = pending.running
|
||||
? MissionState.PendingRunning
|
||||
: MissionState.Pending;
|
||||
return new DownloadStatusSnapshot(state, pending, null, true);
|
||||
}
|
||||
|
||||
FinishedMission finished = getFinishedMission(serviceId, url);
|
||||
if (finished != null) {
|
||||
boolean available = !revalidateFile || isFileAvailable(finished);
|
||||
return new DownloadStatusSnapshot(MissionState.Finished, null, finished, available);
|
||||
}
|
||||
}
|
||||
|
||||
return new DownloadStatusSnapshot(MissionState.None, null, null, false);
|
||||
}
|
||||
|
||||
boolean deleteFinishedMission(int serviceId, String url, boolean deleteFile) {
|
||||
FinishedMission mission = getFinishedMission(serviceId, url);
|
||||
if (mission == null) {
|
||||
return false;
|
||||
}
|
||||
deleteMission(mission, deleteFile);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* runs one or multiple missions in from queue if possible
|
||||
*
|
||||
|
|
|
@ -39,8 +39,6 @@ import androidx.core.content.IntentCompat;
|
|||
import androidx.preference.PreferenceManager;
|
||||
|
||||
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.player.helper.LockManager;
|
||||
import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
|
||||
|
@ -58,8 +56,6 @@ import us.shandian.giga.get.MissionRecoveryInfo;
|
|||
import us.shandian.giga.postprocessing.Postprocessing;
|
||||
import us.shandian.giga.service.DownloadManager.NetworkState;
|
||||
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
|
||||
public class DownloadManagerService extends Service {
|
||||
|
||||
private static final String TAG = "DownloadManagerService";
|
||||
|
@ -85,8 +81,8 @@ public class DownloadManagerService extends Service {
|
|||
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_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 EXTRA_QUALITY_LABEL = "DownloadManagerService.extra.qualityLabel";
|
||||
|
||||
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";
|
||||
|
@ -125,8 +121,6 @@ public class DownloadManagerService extends Service {
|
|||
|
||||
private PendingIntent mOpenDownloadList;
|
||||
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
/**
|
||||
* notify media scanner on downloaded media file ...
|
||||
*
|
||||
|
@ -253,7 +247,6 @@ public class DownloadManagerService extends Service {
|
|||
if (icLauncher != null) icLauncher.recycle();
|
||||
|
||||
mHandler = null;
|
||||
disposables.clear();
|
||||
mManager.pauseAllMissions(true);
|
||||
}
|
||||
|
||||
|
@ -269,18 +262,6 @@ public class DownloadManagerService extends Service {
|
|||
|
||||
switch (msg.what) {
|
||||
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());
|
||||
notifyFinishedDownload(mission.storage.getName());
|
||||
mManager.setFinished(mission);
|
||||
|
@ -384,7 +365,7 @@ public class DownloadManagerService extends Service {
|
|||
char kind, int threads, String source, String psName,
|
||||
String[] psArgs, long nearLength,
|
||||
ArrayList<MissionRecoveryInfo> recoveryInfo,
|
||||
long streamUid, long downloadedEntityId, int serviceId) {
|
||||
long streamUid, int serviceId, String qualityLabel) {
|
||||
final Intent intent = new Intent(context, DownloadManagerService.class)
|
||||
.setAction(Intent.ACTION_RUN)
|
||||
.putExtra(EXTRA_URLS, urls)
|
||||
|
@ -399,8 +380,8 @@ public class DownloadManagerService extends Service {
|
|||
.putExtra(EXTRA_PATH, storage.getUri())
|
||||
.putExtra(EXTRA_STORAGE_TAG, storage.getTag())
|
||||
.putExtra(EXTRA_STREAM_UID, streamUid)
|
||||
.putExtra(EXTRA_DOWNLOADED_ID, downloadedEntityId)
|
||||
.putExtra(EXTRA_SERVICE_ID, serviceId);
|
||||
.putExtra(EXTRA_SERVICE_ID, serviceId)
|
||||
.putExtra(EXTRA_QUALITY_LABEL, qualityLabel);
|
||||
|
||||
context.startService(intent);
|
||||
}
|
||||
|
@ -417,8 +398,8 @@ public class DownloadManagerService extends Service {
|
|||
long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0);
|
||||
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);
|
||||
String qualityLabel = intent.getStringExtra(EXTRA_QUALITY_LABEL);
|
||||
final var recovery = IntentCompat.getParcelableArrayListExtra(intent, EXTRA_RECOVERY_INFO,
|
||||
MissionRecoveryInfo.class);
|
||||
Objects.requireNonNull(recovery);
|
||||
|
@ -442,8 +423,8 @@ public class DownloadManagerService extends Service {
|
|||
mission.nearLength = nearLength;
|
||||
mission.recoveryInfo = recovery.toArray(new MissionRecoveryInfo[0]);
|
||||
mission.streamUid = streamUid;
|
||||
mission.downloadedEntityId = downloadedEntityId;
|
||||
mission.serviceId = serviceId;
|
||||
mission.qualityLabel = qualityLabel;
|
||||
|
||||
if (ps != null)
|
||||
ps.setTemporalDir(DownloadManager.pickAvailableTemporalDir(this));
|
||||
|
@ -615,6 +596,15 @@ public class DownloadManagerService extends Service {
|
|||
mDownloadNotificationEnable = enable;
|
||||
}
|
||||
|
||||
public DownloadManager.DownloadStatusSnapshot getDownloadStatus(int serviceId, String source,
|
||||
boolean revalidateFile) {
|
||||
return mManager.getDownloadStatus(serviceId, source, revalidateFile);
|
||||
}
|
||||
|
||||
public boolean deleteFinishedMission(int serviceId, String source, boolean deleteFile) {
|
||||
return mManager.deleteFinishedMission(serviceId, source, deleteFile);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
<?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>
|
|
@ -551,27 +551,21 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/detail_download_status_chip"
|
||||
style="@style/Widget.MaterialComponents.Chip.Action"
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/detail_download_status_compose"
|
||||
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" />
|
||||
android:visibility="visible" />
|
||||
|
||||
<View
|
||||
android:id="@+id/detail_meta_info_separator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:layout_below="@id/detail_download_status_chip"
|
||||
android:layout_below="@id/detail_download_status_compose"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:background="?attr/separator_color" />
|
||||
|
|
|
@ -25,10 +25,8 @@
|
|||
<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>
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
<?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