mirror of
https://github.com/NiciDieNase/chaosflix
synced 2024-11-26 22:20:24 +00:00
load thumbs from api
This commit is contained in:
parent
6c155e2706
commit
6d5b9dc791
15 changed files with 1136 additions and 220 deletions
|
@ -149,8 +149,8 @@ dependencies {
|
|||
kapt "androidx.room:room-compiler:$roomVersion"
|
||||
api "androidx.room:room-ktx:$roomVersion"
|
||||
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.6.4'
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.6.4'
|
||||
api 'com.squareup.retrofit2:retrofit:2.6.4'
|
||||
api 'com.squareup.retrofit2:converter-gson:2.6.4'
|
||||
|
||||
api "com.google.code.gson:gson:2.8.6"
|
||||
api 'com.google.android.exoplayer:exoplayer:2.9.6'
|
||||
|
|
|
@ -0,0 +1,777 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 8,
|
||||
"identityHash": "cd02652f6c74b9e0a5371cb34f851365",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "conference",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `conferenceGroupId` INTEGER NOT NULL, `acronym` TEXT NOT NULL, `aspectRatio` TEXT NOT NULL, `title` TEXT NOT NULL, `slug` TEXT NOT NULL, `webgenLocation` TEXT NOT NULL, `scheduleUrl` TEXT, `logoUrl` TEXT NOT NULL, `imagesUrl` TEXT NOT NULL, `recordingsUrl` TEXT NOT NULL, `url` TEXT NOT NULL, `updatedAt` TEXT NOT NULL, `tagsUsefull` INTEGER NOT NULL, `lastReleasedAt` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "conferenceGroupId",
|
||||
"columnName": "conferenceGroupId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "acronym",
|
||||
"columnName": "acronym",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "aspectRatio",
|
||||
"columnName": "aspectRatio",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "slug",
|
||||
"columnName": "slug",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "webgenLocation",
|
||||
"columnName": "webgenLocation",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "scheduleUrl",
|
||||
"columnName": "scheduleUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "logoUrl",
|
||||
"columnName": "logoUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "imagesUrl",
|
||||
"columnName": "imagesUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "recordingsUrl",
|
||||
"columnName": "recordingsUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "updatedAt",
|
||||
"columnName": "updatedAt",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tagsUsefull",
|
||||
"columnName": "tagsUsefull",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastReleasedAt",
|
||||
"columnName": "lastReleasedAt",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_conference_acronym",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"acronym"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_conference_acronym` ON `${TABLE_NAME}` (`acronym`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "event",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `conferenceId` INTEGER NOT NULL, `conference` TEXT NOT NULL, `guid` TEXT NOT NULL, `title` TEXT NOT NULL, `subtitle` TEXT, `slug` TEXT NOT NULL, `link` TEXT, `description` TEXT, `originalLanguage` TEXT NOT NULL, `date` TEXT, `releaseDate` TEXT NOT NULL, `updatedAt` TEXT NOT NULL, `length` INTEGER NOT NULL, `thumbUrl` TEXT NOT NULL, `posterUrl` TEXT NOT NULL, `frontendLink` TEXT, `url` TEXT NOT NULL, `conferenceUrl` TEXT NOT NULL, `isPromoted` INTEGER NOT NULL, `viewCount` INTEGER NOT NULL, `persons` TEXT, `tags` TEXT, `timelineUrl` TEXT NOT NULL, `thumbnailsUrl` TEXT NOT NULL, FOREIGN KEY(`conferenceId`) REFERENCES `conference`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "conferenceId",
|
||||
"columnName": "conferenceId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "conference",
|
||||
"columnName": "conference",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "guid",
|
||||
"columnName": "guid",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "subtitle",
|
||||
"columnName": "subtitle",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "slug",
|
||||
"columnName": "slug",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "link",
|
||||
"columnName": "link",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "originalLanguage",
|
||||
"columnName": "originalLanguage",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "date",
|
||||
"columnName": "date",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "releaseDate",
|
||||
"columnName": "releaseDate",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "updatedAt",
|
||||
"columnName": "updatedAt",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "length",
|
||||
"columnName": "length",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumbUrl",
|
||||
"columnName": "thumbUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "posterUrl",
|
||||
"columnName": "posterUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "frontendLink",
|
||||
"columnName": "frontendLink",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "conferenceUrl",
|
||||
"columnName": "conferenceUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPromoted",
|
||||
"columnName": "isPromoted",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "viewCount",
|
||||
"columnName": "viewCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "persons",
|
||||
"columnName": "persons",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "tags",
|
||||
"columnName": "tags",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "timelineUrl",
|
||||
"columnName": "timelineUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumbnailsUrl",
|
||||
"columnName": "thumbnailsUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_event_guid",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"guid"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_event_guid` ON `${TABLE_NAME}` (`guid`)"
|
||||
},
|
||||
{
|
||||
"name": "index_event_frontendLink",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"frontendLink"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_event_frontendLink` ON `${TABLE_NAME}` (`frontendLink`)"
|
||||
},
|
||||
{
|
||||
"name": "index_event_conferenceId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"conferenceId"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_event_conferenceId` ON `${TABLE_NAME}` (`conferenceId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "conference",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"conferenceId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "recording",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `eventId` INTEGER NOT NULL, `size` INTEGER NOT NULL, `length` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `language` TEXT NOT NULL, `filename` TEXT NOT NULL, `state` TEXT NOT NULL, `folder` TEXT NOT NULL, `isHighQuality` INTEGER NOT NULL, `width` INTEGER NOT NULL, `height` INTEGER NOT NULL, `updatedAt` TEXT NOT NULL, `recordingUrl` TEXT NOT NULL, `url` TEXT NOT NULL, `eventUrl` TEXT NOT NULL, `conferenceUrl` TEXT NOT NULL, `backendId` INTEGER NOT NULL, FOREIGN KEY(`eventId`) REFERENCES `event`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "eventId",
|
||||
"columnName": "eventId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "size",
|
||||
"columnName": "size",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "length",
|
||||
"columnName": "length",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mimeType",
|
||||
"columnName": "mimeType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "language",
|
||||
"columnName": "language",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "filename",
|
||||
"columnName": "filename",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "state",
|
||||
"columnName": "state",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "folder",
|
||||
"columnName": "folder",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isHighQuality",
|
||||
"columnName": "isHighQuality",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "width",
|
||||
"columnName": "width",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "height",
|
||||
"columnName": "height",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "updatedAt",
|
||||
"columnName": "updatedAt",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "recordingUrl",
|
||||
"columnName": "recordingUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "eventUrl",
|
||||
"columnName": "eventUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "conferenceUrl",
|
||||
"columnName": "conferenceUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "backendId",
|
||||
"columnName": "backendId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_recording_eventId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"eventId"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_recording_eventId` ON `${TABLE_NAME}` (`eventId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_recording_url",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"url"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_recording_url` ON `${TABLE_NAME}` (`url`)"
|
||||
},
|
||||
{
|
||||
"name": "index_recording_backendId",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"backendId"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_recording_backendId` ON `${TABLE_NAME}` (`backendId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "event",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"eventId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "related",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `parentEventId` INTEGER NOT NULL, `relatedEventGuid` TEXT NOT NULL, `weight` INTEGER NOT NULL, FOREIGN KEY(`parentEventId`) REFERENCES `event`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "parentEventId",
|
||||
"columnName": "parentEventId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "relatedEventGuid",
|
||||
"columnName": "relatedEventGuid",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "weight",
|
||||
"columnName": "weight",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_related_parentEventId_relatedEventGuid",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"parentEventId",
|
||||
"relatedEventGuid"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_related_parentEventId_relatedEventGuid` ON `${TABLE_NAME}` (`parentEventId`, `relatedEventGuid`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "event",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"parentEventId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "conference_group",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `order_index` INTEGER NOT NULL, `name` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "index",
|
||||
"columnName": "order_index",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_conference_group_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"name"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_conference_group_name` ON `${TABLE_NAME}` (`name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "playback_progress",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `event_guid` TEXT NOT NULL, `progress` INTEGER NOT NULL, `watch_date` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "eventGuid",
|
||||
"columnName": "event_guid",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "progress",
|
||||
"columnName": "progress",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "watchDate",
|
||||
"columnName": "watch_date",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_playback_progress_event_guid",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"event_guid"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playback_progress_event_guid` ON `${TABLE_NAME}` (`event_guid`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "watchlist_item",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `event_guid` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "eventGuid",
|
||||
"columnName": "event_guid",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_watchlist_item_event_guid",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"event_guid"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_watchlist_item_event_guid` ON `${TABLE_NAME}` (`event_guid`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "offline_event",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `event_guid` TEXT NOT NULL, `recording_id` INTEGER NOT NULL, `download_reference` INTEGER NOT NULL, `local_path` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "eventGuid",
|
||||
"columnName": "event_guid",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "recordingId",
|
||||
"columnName": "recording_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "downloadReference",
|
||||
"columnName": "download_reference",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "localPath",
|
||||
"columnName": "local_path",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_offline_event_event_guid",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"event_guid"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_offline_event_event_guid` ON `${TABLE_NAME}` (`event_guid`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "recommendation",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `event_guid` TEXT NOT NULL, `channel` TEXT NOT NULL, `programm_id` INTEGER NOT NULL, `dismissed` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "eventGuid",
|
||||
"columnName": "event_guid",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "channel",
|
||||
"columnName": "channel",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "programmId",
|
||||
"columnName": "programm_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dismissed",
|
||||
"columnName": "dismissed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_recommendation_event_guid_channel",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"event_guid",
|
||||
"channel"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_recommendation_event_guid_channel` ON `${TABLE_NAME}` (`event_guid`, `channel`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cd02652f6c74b9e0a5371cb34f851365')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -40,7 +40,7 @@ import de.nicidienase.chaosflix.common.userdata.entities.watchlist.WatchlistItem
|
|||
OfflineEvent::class,
|
||||
Recommendation::class
|
||||
],
|
||||
version = 7,
|
||||
version = 8,
|
||||
exportSchema = true)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class ChaosflixDatabase : RoomDatabase() {
|
||||
|
@ -62,7 +62,8 @@ abstract class ChaosflixDatabase : RoomDatabase() {
|
|||
ChaosflixDatabase::class.java, "mediaccc.de")
|
||||
.addMigrations(
|
||||
ChaosflixDatabase.migration_5_6,
|
||||
ChaosflixDatabase.migration_6_7
|
||||
ChaosflixDatabase.migration_6_7,
|
||||
ChaosflixDatabase.migration_7_8
|
||||
)
|
||||
.fallbackToDestructiveMigrationFrom(1, 2, 3, 4)
|
||||
.build()
|
||||
|
@ -118,5 +119,12 @@ abstract class ChaosflixDatabase : RoomDatabase() {
|
|||
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS index_recommendation_event_guid_channel ON recommendation (event_guid, channel)")
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_7_8 = object : Migration(7, 8) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE event ADD COLUMN timelineUrl TEXT NOT NULL DEFAULT ''")
|
||||
database.execSQL("ALTER TABLE event ADD COLUMN thumbnailsUrl TEXT NOT NULL DEFAULT ''")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
package de.nicidienase.chaosflix.common.mediadata
|
||||
|
||||
import java.util.LinkedList
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
|
||||
class ThumbnailParser(private val okHttpClient: OkHttpClient) {
|
||||
|
||||
fun parse(uri: String): List<ThumbnailInfo> {
|
||||
|
||||
val request = Request.Builder().url(uri).build()
|
||||
val lines = LinkedList(okHttpClient.newCall(request).execute().body()?.byteStream()?.bufferedReader()?.readLines())
|
||||
|
||||
val results = mutableListOf<Pair<String, String>>()
|
||||
while (lines.peek().isNullOrBlank() || lines.peek().equals("WEBVTT")) {
|
||||
lines.pop()
|
||||
}
|
||||
while (lines.isNotEmpty()) {
|
||||
val times = lines.pop()
|
||||
val datalines = mutableListOf<String>()
|
||||
while (lines.peek().isNotEmpty()) {
|
||||
datalines.add(lines.pop())
|
||||
}
|
||||
results.add(times to datalines.first())
|
||||
while (!lines.isEmpty() && (lines.peek().isNullOrBlank() || lines.peek().equals("WEBVTT"))) {
|
||||
lines.pop()
|
||||
}
|
||||
}
|
||||
return convert(uri, results)
|
||||
}
|
||||
|
||||
fun convert(uri: String, input: List<Pair<String, String>>): List<ThumbnailInfo> {
|
||||
val baseUri = uri.substringBeforeLast("/")
|
||||
return input.map {
|
||||
val split = it.first.split(" --> ")
|
||||
val uri = "$baseUri/${it.second}"
|
||||
ThumbnailInfo(
|
||||
timestampToMillis(split.first()),
|
||||
timestampToMillis(split.last()),
|
||||
uri
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
internal fun timestampToMillis(timestamp: String): Long {
|
||||
val split1 = timestamp.split(".")
|
||||
var result = split1.last().toLong()
|
||||
|
||||
assert(split1.size == 2)
|
||||
val split2 = split1.first().split(":").reversed()
|
||||
var factor: Long = 1000L
|
||||
for (item in split2) {
|
||||
result += item.toLong() * factor
|
||||
factor *= 60
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
data class ThumbnailInfo(
|
||||
val startTime: Long,
|
||||
val endTime: Long,
|
||||
val thumb: String
|
||||
)
|
||||
}
|
|
@ -27,6 +27,10 @@ data class EventDto(
|
|||
var thumbUrl: String? = "",
|
||||
@SerializedName("poster_url")
|
||||
var posterUrl: String = "",
|
||||
@SerializedName("timeline_url")
|
||||
var timelineUrl: String = "",
|
||||
@SerializedName("thumbnails_url")
|
||||
var thumbnailsUrl: String = "",
|
||||
@SerializedName("frontend_link")
|
||||
var frontendLink: String? = "",
|
||||
var url: String = "",
|
||||
|
@ -35,11 +39,7 @@ data class EventDto(
|
|||
var recordings: List<RecordingDto>?,
|
||||
var related: List<RelatedEventDto>?,
|
||||
@SerializedName("promoted")
|
||||
var isPromoted: Boolean = false,
|
||||
@SerializedName("timeline_url")
|
||||
var timelineUrl: String,
|
||||
@SerializedName("thumbnails_url")
|
||||
var thumbnailsUrl: String
|
||||
var isPromoted: Boolean = false
|
||||
) : Comparable<EventDto> {
|
||||
|
||||
var eventID: Long
|
||||
|
|
|
@ -44,26 +44,20 @@ data class Event(
|
|||
var url: String = "",
|
||||
var conferenceUrl: String = "",
|
||||
var isPromoted: Boolean = false,
|
||||
|
||||
var viewCount: Int = 0,
|
||||
var persons: Array<String>? = null,
|
||||
|
||||
var tags: Array<String>? = null,
|
||||
@Ignore
|
||||
var related: List<RelatedEvent>? = null,
|
||||
@Ignore
|
||||
var recordings: List<Recording>? = null
|
||||
) : Parcelable, Comparable<Event> {
|
||||
var recordings: List<Recording>? = null,
|
||||
var timelineUrl: String = "",
|
||||
var thumbnailsUrl: String = ""
|
||||
) : Comparable<Event>, Parcelable {
|
||||
|
||||
@Ignore
|
||||
var progress: Long = 0
|
||||
|
||||
override fun compareTo(other: Event): Int = title.compareTo(other.title)
|
||||
|
||||
fun getFilteredProperties(): List<String> {
|
||||
return listOfNotNull(title, subtitle, description, getSpeakerString())
|
||||
}
|
||||
|
||||
constructor(parcel: Parcel) : this(
|
||||
parcel.readLong(),
|
||||
parcel.readLong(),
|
||||
|
@ -89,7 +83,17 @@ data class Event(
|
|||
parcel.createStringArray(),
|
||||
parcel.createStringArray(),
|
||||
parcel.createTypedArrayList(RelatedEvent),
|
||||
parcel.createTypedArrayList(Recording))
|
||||
parcel.createTypedArrayList(Recording),
|
||||
parcel.readString() ?: "",
|
||||
parcel.readString() ?: "") {
|
||||
progress = parcel.readLong()
|
||||
}
|
||||
|
||||
override fun compareTo(other: Event): Int = title.compareTo(other.title)
|
||||
|
||||
fun getFilteredProperties(): List<String> {
|
||||
return listOfNotNull(title, subtitle, description, getSpeakerString())
|
||||
}
|
||||
|
||||
@Ignore
|
||||
constructor(event: EventDto, conferenceId: Long = 0) : this(
|
||||
|
@ -115,7 +119,9 @@ data class Event(
|
|||
persons = event.persons,
|
||||
tags = event.tags?.filterNotNull()?.toTypedArray(),
|
||||
related = event.related?.map { RelatedEvent(event.eventID, it) },
|
||||
recordings = event.recordings?.map { Recording(it) }
|
||||
recordings = event.recordings?.map { Recording(it) },
|
||||
timelineUrl = event.timelineUrl,
|
||||
thumbnailsUrl = event.thumbnailsUrl
|
||||
)
|
||||
|
||||
fun getExtendedDescription(): Spanned {
|
||||
|
@ -157,6 +163,9 @@ data class Event(
|
|||
parcel.writeStringArray(tags)
|
||||
parcel.writeTypedList(related)
|
||||
parcel.writeTypedList(recordings)
|
||||
parcel.writeString(timelineUrl)
|
||||
parcel.writeString(thumbnailsUrl)
|
||||
parcel.writeLong(progress)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
|
|
|
@ -17,7 +17,7 @@ class ApiFactory private constructor(apiUrl: String, cache: File? = null) {
|
|||
private val chaosflixUserAgent: String by lazy { buildUserAgent() }
|
||||
private val gsonConverterFactory: GsonConverterFactory by lazy { GsonConverterFactory.create(Gson()) }
|
||||
|
||||
private val client: OkHttpClient by lazy {
|
||||
val client: OkHttpClient by lazy {
|
||||
OkHttpClient.Builder()
|
||||
.connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
|
||||
.readTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
|
||||
|
|
|
@ -4,12 +4,14 @@ import androidx.lifecycle.LiveData
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.nicidienase.chaosflix.common.ChaosflixDatabase
|
||||
import de.nicidienase.chaosflix.common.mediadata.ThumbnailParser
|
||||
import de.nicidienase.chaosflix.common.userdata.entities.progress.PlaybackProgress
|
||||
import java.util.Date
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class PlayerViewModel(val database: ChaosflixDatabase) : ViewModel() {
|
||||
class PlayerViewModel(private val database: ChaosflixDatabase, private val thumbnailParser: ThumbnailParser) : ViewModel() {
|
||||
|
||||
private lateinit var eventGuid: String
|
||||
|
||||
|
@ -48,4 +50,13 @@ class PlayerViewModel(val database: ChaosflixDatabase) : ViewModel() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getThumbInfo(): List<ThumbnailParser.ThumbnailInfo>? = withContext(Dispatchers.IO) {
|
||||
val event = database.eventDao().findEventByGuidSync(eventGuid)
|
||||
return@withContext if (event != null && event.thumbnailsUrl.isNotBlank()) {
|
||||
thumbnailParser.parse(event.thumbnailsUrl)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,11 +13,12 @@ import de.nicidienase.chaosflix.common.ResourcesFacade
|
|||
import de.nicidienase.chaosflix.common.SingletonHolder
|
||||
import de.nicidienase.chaosflix.common.mediadata.MediaRepository
|
||||
import de.nicidienase.chaosflix.common.mediadata.StreamingRepository
|
||||
import de.nicidienase.chaosflix.common.mediadata.ThumbnailParser
|
||||
import de.nicidienase.chaosflix.common.mediadata.network.ApiFactory
|
||||
|
||||
class ViewModelFactory private constructor(context: Context) : ViewModelProvider.Factory {
|
||||
|
||||
private val apiFactory = ApiFactory.getInstance(context.resources.getString(R.string.recording_url), context.cacheDir)
|
||||
val apiFactory = ApiFactory.getInstance(context.resources.getString(R.string.recording_url), context.cacheDir)
|
||||
|
||||
private val database by lazy { ChaosflixDatabase.getInstance(context) }
|
||||
private val streamingRepository by lazy { StreamingRepository(apiFactory.streamingApi) }
|
||||
|
@ -31,6 +32,7 @@ class ViewModelFactory private constructor(context: Context) : ViewModelProvider
|
|||
)
|
||||
private val externalFilesDir = Environment.getExternalStorageDirectory()
|
||||
private val resourcesFacade by lazy { ResourcesFacade(context) }
|
||||
private val thumbnailParser by lazy { ThumbnailParser(apiFactory.client) }
|
||||
val mediaRepository by lazy { MediaRepository(apiFactory.recordingApi, database) }
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
|
@ -43,7 +45,7 @@ class ViewModelFactory private constructor(context: Context) : ViewModelProvider
|
|||
streamingRepository,
|
||||
preferencesManager,
|
||||
resourcesFacade) as T
|
||||
PlayerViewModel::class.java -> PlayerViewModel(database) as T
|
||||
PlayerViewModel::class.java -> PlayerViewModel(database, thumbnailParser) as T
|
||||
DetailsViewModel::class.java -> DetailsViewModel(
|
||||
database,
|
||||
offlineItemManager,
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
package de.nicidienase.chaosflix.common.mediadata
|
||||
|
||||
import java.util.concurrent.TimeUnit
|
||||
import okhttp3.OkHttpClient
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.Matchers
|
||||
import org.hamcrest.number.OrderingComparison.greaterThan
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Disabled
|
||||
|
||||
internal class ThumbnailParserTest {
|
||||
|
||||
private val okHttpClient: OkHttpClient by lazy {
|
||||
OkHttpClient.Builder()
|
||||
.connectTimeout(5, TimeUnit.SECONDS)
|
||||
.readTimeout(5, TimeUnit.SECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
@Disabled
|
||||
@Test
|
||||
fun test() {
|
||||
val parse = ThumbnailParser("https://static.media.ccc.de/media/events/gpn/gpn19/67-hd.thumbnails.vtt", okHttpClient).parse()
|
||||
assertThat(parse.size, greaterThan(999))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun timestampConverter() {
|
||||
assertThat(ThumbnailParser.timestampToMillis("00:00:00.500"), Matchers.`is`(500L))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun timestampConverter2() {
|
||||
assertThat(ThumbnailParser.timestampToMillis("00:00:00.500"), Matchers.`is`(500L))
|
||||
}
|
||||
@Test
|
||||
fun timestampConverter3() {
|
||||
assertThat(ThumbnailParser.timestampToMillis("00:01:00.000"), Matchers.`is`(60 * 1000L))
|
||||
}
|
||||
@Test
|
||||
fun timestampConverter4() {
|
||||
assertThat(ThumbnailParser.timestampToMillis("01:00:00.000"), Matchers.`is`(60 * 60 * 1000L))
|
||||
}
|
||||
@Test
|
||||
fun timestampConverter5() {
|
||||
assertThat(ThumbnailParser.timestampToMillis("03:05:11.111"), Matchers.`is`(11111111L))
|
||||
}
|
||||
}
|
|
@ -2,52 +2,31 @@ package de.nicidienase.chaosflix.leanback.detail
|
|||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.os.Build
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.leanback.media.PlaybackGlue
|
||||
import androidx.leanback.widget.PlaybackSeekDataProvider
|
||||
import de.nicidienase.chaosflix.common.AnalyticsWrapper
|
||||
import de.nicidienase.chaosflix.common.AnalyticsWrapperImpl
|
||||
import de.nicidienase.chaosflix.common.mediadata.network.ApiFactory
|
||||
import com.bumptech.glide.Glide
|
||||
import de.nicidienase.chaosflix.common.mediadata.ThumbnailParser
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.yield
|
||||
|
||||
class ChaosflixSeekDataProvider(
|
||||
val context: Context,
|
||||
val length: Long,
|
||||
val url: String
|
||||
val thumbInfos: List<ThumbnailParser.ThumbnailInfo>
|
||||
) : PlaybackSeekDataProvider() {
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO + Job())
|
||||
|
||||
private var canceled = true
|
||||
private var generateThumbsJob: Job? = null
|
||||
|
||||
private var mediaMetadataRetriever = MediaMetadataRetriever()
|
||||
|
||||
private val drawableMap: MutableMap<String, Bitmap> = mutableMapOf()
|
||||
private val thumbnails: MutableMap<Long, Bitmap> = mutableMapOf()
|
||||
private val dummyThumbnails: MutableMap<Long, Bitmap> = mutableMapOf()
|
||||
|
||||
private val calcTimes: MutableList<Long> = mutableListOf()
|
||||
|
||||
private val positions: LongArray by lazy {
|
||||
val list = mutableListOf<Long>()
|
||||
for (i in 0..(length * 1000) step calculateInterval(length)) {
|
||||
list.add(i)
|
||||
}
|
||||
list.add(length * 1000)
|
||||
Log.d(TAG, "Calculated ${list.size} steps")
|
||||
return@lazy list.toLongArray()
|
||||
(0..(length * 1000) step calculateInterval(length)).takeWhile { it < length * 1000 }.toLongArray()
|
||||
}
|
||||
|
||||
private fun calculateInterval(length: Long): Long {
|
||||
|
@ -59,199 +38,77 @@ class ChaosflixSeekDataProvider(
|
|||
}
|
||||
}
|
||||
|
||||
fun initialize() {
|
||||
if (canceled) {
|
||||
canceled = false
|
||||
Log.d(TAG, "Retriever was canceled before, reinitializing")
|
||||
mediaMetadataRetriever = MediaMetadataRetriever()
|
||||
try {
|
||||
mediaMetadataRetriever.setDataSource(url, mapOf("User-Agent" to ApiFactory.buildUserAgent()))
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Error: ${ex.message}", ex)
|
||||
}
|
||||
generateThumbs()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSeekPositions(): LongArray {
|
||||
initialize()
|
||||
return positions
|
||||
}
|
||||
|
||||
private fun generateThumbs() {
|
||||
generateThumbsJob = scope.launch {
|
||||
for (i in positions.indices) {
|
||||
if (!thumbnails.containsKey(positions[i])) {
|
||||
val dummyThumbnail = createDummyThumbnail(i)
|
||||
dummyThumbnails[positions[i]] = dummyThumbnail
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "Added Dummy-Thumbs")
|
||||
yield()
|
||||
for (i in positions.indices) {
|
||||
if (!thumbnails.containsKey(positions[i])) {
|
||||
val bitmap = createBitmapForIndex(i)
|
||||
if (bitmap != null) {
|
||||
thumbnails[positions[i]] = bitmap
|
||||
}
|
||||
yield()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getThumbnail(index: Int, callback: ResultCallback?) {
|
||||
val timestamp = positions[index]
|
||||
when {
|
||||
thumbnails.contains(positions[index]) -> {
|
||||
thumbnails.contains(timestamp) -> {
|
||||
Log.d(TAG, "Thumbnail match ($index/${positions.size})")
|
||||
callback?.onThumbnailLoaded(thumbnails[positions[index]], index)
|
||||
}
|
||||
dummyThumbnails.contains(positions[index]) -> {
|
||||
callback?.onThumbnailLoaded(dummyThumbnails[positions[index]], index)
|
||||
scope.launch {
|
||||
val thumb = createBitmapForIndex(index)
|
||||
if (thumb != null) {
|
||||
thumbnails[positions[index]] = thumb
|
||||
withContext(Dispatchers.Main) {
|
||||
callback?.onThumbnailLoaded(thumb, index)
|
||||
}
|
||||
}
|
||||
}
|
||||
callback?.onThumbnailLoaded(thumbnails[timestamp], index)
|
||||
}
|
||||
else -> scope.launch {
|
||||
val dummyThumb = createDummyThumbnail(index)
|
||||
dummyThumbnails[positions[index]] = dummyThumb
|
||||
try {
|
||||
val thumbInfo = thumbInfos.first { it.startTime <= timestamp && timestamp <= it.endTime }
|
||||
loadImageForUri(thumbInfo.thumb)?.let {
|
||||
thumbnails[timestamp] = it
|
||||
withContext(Dispatchers.Main) {
|
||||
callback?.onThumbnailLoaded(dummyThumb, index)
|
||||
callback?.onThumbnailLoaded(it, index)
|
||||
}
|
||||
scope.launch {
|
||||
val thumb = createBitmapForIndex(index)
|
||||
if (thumb != null) {
|
||||
thumbnails[positions[index]] = thumb
|
||||
withContext(Dispatchers.Main) {
|
||||
callback?.onThumbnailLoaded(thumb, index)
|
||||
}
|
||||
} catch (e: NoSuchElementException) {
|
||||
Log.e(TAG, e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createBitmapForIndex(index: Int): Bitmap? {
|
||||
if (canceled) {
|
||||
return null
|
||||
}
|
||||
val startTime = System.currentTimeMillis()
|
||||
val pos = positions[index] * 1000
|
||||
val thumb: Bitmap = try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
mediaMetadataRetriever.getScaledFrameAtTime(pos, MediaMetadataRetriever.OPTION_CLOSEST_SYNC,
|
||||
THUMB_WIDTH, THUMB_HEIGHT)
|
||||
} else {
|
||||
mediaMetadataRetriever.getFrameAtTime(pos, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
|
||||
} ?: Bitmap.createBitmap(THUMB_WIDTH, THUMB_HEIGHT, Bitmap.Config.ARGB_8888)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Error: ${ex.message}", ex)
|
||||
return Bitmap.createBitmap(10, 10, Bitmap.Config.RGB_565)
|
||||
}
|
||||
Log.d(TAG, "Thumb size: ${thumb.width}x${thumb.height}")
|
||||
|
||||
val seconds = positions[index] / 1000
|
||||
val time = formatTime(seconds)
|
||||
drawStringToBitmap(thumb, time)
|
||||
|
||||
val duration = System.currentTimeMillis() - startTime
|
||||
calcTimes.add(duration)
|
||||
Log.d(TAG, "Adding Thumbnail ($index/${positions.size}) (took ${duration}ms)")
|
||||
return thumb
|
||||
}
|
||||
|
||||
private fun formatTime(seconds: Long): String {
|
||||
val s = seconds % 60
|
||||
val m = (seconds / 60) % 60
|
||||
val h = seconds / 3600
|
||||
return if (h > 0) {
|
||||
String.format("%d:%02d:%02d", h, m, s)
|
||||
} else {
|
||||
String.format("%d:%02d", m, s)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawStringToBitmap(thumb: Bitmap, time: String) {
|
||||
val canvas = Canvas(thumb)
|
||||
val paint = Paint()
|
||||
|
||||
val textHeight = (canvas.height / 5).toFloat()
|
||||
paint.textSize = textHeight
|
||||
paint.color = Color.parseColor("#DD444444")
|
||||
val textWidth = paint.measureText(time)
|
||||
canvas.drawRoundRect(
|
||||
0F,
|
||||
canvas.height - (textHeight),
|
||||
textWidth * 1.1F,
|
||||
canvas.height.toFloat(),
|
||||
textHeight * 0.2F,
|
||||
textHeight * 0.2F,
|
||||
paint
|
||||
)
|
||||
paint.color = Color.WHITE
|
||||
canvas.drawText(time,
|
||||
textHeight * 0.1F,
|
||||
canvas.height.toFloat() - (textHeight * 0.1F),
|
||||
paint)
|
||||
}
|
||||
|
||||
private fun createDummyThumbnail(index: Int): Bitmap {
|
||||
val seconds = positions[index] / 1000
|
||||
val time = formatTime(seconds)
|
||||
val bitmap = Bitmap.createBitmap(THUMB_WIDTH, THUMB_HEIGHT, Bitmap.Config.ARGB_8888)
|
||||
|
||||
drawStringToBitmap(bitmap, time)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
override fun reset() {
|
||||
Log.d(TAG, "SeekData reset Started")
|
||||
canceled = true
|
||||
GlobalScope.launch {
|
||||
generateThumbsJob?.cancelAndJoin()
|
||||
try {
|
||||
mediaMetadataRetriever.release()
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Error: ${ex.message}", ex)
|
||||
}
|
||||
}
|
||||
AnalyticsWrapperImpl.addAnalyticsEvent(AnalyticsWrapper.thumbnailsStatEvent,
|
||||
mapOf(
|
||||
"avg" to calcTimes.toLongArray().average().toLong().toString(),
|
||||
"positions_count" to positions.size.toString(),
|
||||
"calculated_count" to calcTimes.size.toString()
|
||||
)
|
||||
)
|
||||
calcTimes.clear()
|
||||
thumbnails.clear()
|
||||
dummyThumbnails.clear()
|
||||
drawableMap.clear()
|
||||
Log.d(TAG, "SeekData reset Done")
|
||||
super.reset()
|
||||
}
|
||||
|
||||
private suspend fun loadImageForUri(uri: String): Bitmap? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val imgUri = Uri.parse(uri)
|
||||
val sourceBitmap = drawableMap[imgUri.path] ?: Glide.with(context)
|
||||
.asBitmap()
|
||||
.load(imgUri)
|
||||
// .submit()
|
||||
.submit(960, 990)
|
||||
.get()
|
||||
val params = imgUri.getQueryParameter("xywh")?.split(",")
|
||||
return@withContext if (params?.size == 4) {
|
||||
val (x, y, w, h) = params.map { it.toInt() }
|
||||
Bitmap.createBitmap(sourceBitmap, x, y, w, h)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (ex: RuntimeException) {
|
||||
Log.e(TAG, ex.message, ex)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = ChaosflixSeekDataProvider::class.java.simpleName
|
||||
|
||||
private const val THUMB_WIDTH = 480
|
||||
private const val THUMB_HEIGHT = 270
|
||||
|
||||
fun setSeekProvider(
|
||||
glue: ChaosMediaPlayerGlue,
|
||||
context: Context,
|
||||
length: Long,
|
||||
url: String
|
||||
thumbs: List<ThumbnailParser.ThumbnailInfo>
|
||||
) {
|
||||
val chaosflixSeekDataProvider = ChaosflixSeekDataProvider(context, length, url)
|
||||
val chaosflixSeekDataProvider = ChaosflixSeekDataProvider(context, length, thumbs)
|
||||
if (glue.isPrepared) {
|
||||
glue.seekProvider = chaosflixSeekDataProvider
|
||||
glue.isSeekEnabled = true
|
||||
chaosflixSeekDataProvider.initialize()
|
||||
} else {
|
||||
glue.addPlayerCallback(object : PlaybackGlue.PlayerCallback() {
|
||||
override fun onPreparedStateChanged(glue: PlaybackGlue?) {
|
||||
|
@ -260,7 +117,6 @@ class ChaosflixSeekDataProvider(
|
|||
(glue as ChaosMediaPlayerGlue).seekProvider =
|
||||
chaosflixSeekDataProvider
|
||||
glue.isSeekEnabled = true
|
||||
chaosflixSeekDataProvider.initialize()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -196,7 +196,7 @@ class EventDetailsFragment : DetailsSupportFragment() {
|
|||
val progress: Long? = state.data?.getLong(DetailsViewModel.PROGRESS)
|
||||
if (recording != null) {
|
||||
if (parcelable != null) {
|
||||
prepareSeekProvider(parcelable.length, url ?: recording.recordingUrl)
|
||||
prepareSeekProvider(parcelable.length)
|
||||
}
|
||||
preparePlayer(recording.recordingUrl)
|
||||
playerAdapter.play()
|
||||
|
@ -279,14 +279,18 @@ class EventDetailsFragment : DetailsSupportFragment() {
|
|||
selectDialog?.show()
|
||||
}
|
||||
|
||||
private fun prepareSeekProvider(length: Long, url: String) {
|
||||
private fun prepareSeekProvider(length: Long) {
|
||||
lifecycleScope.launch {
|
||||
playerViewModel.getThumbInfo()?.let {
|
||||
ChaosflixSeekDataProvider.setSeekProvider(
|
||||
playerGlue,
|
||||
requireContext(),
|
||||
length,
|
||||
url
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || activity?.isInPictureInPictureMode == false) {
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
package de.nicidienase.chaosflix.touch
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import de.nicidienase.chaosflix.common.mediadata.ThumbnailParser
|
||||
import de.nicidienase.chaosflix.common.viewmodel.ViewModelFactory
|
||||
import de.nicidienase.chaosflix.touch.databinding.FragmentThumbsTestBinding
|
||||
import de.nicidienase.chaosflix.touch.databinding.ItemThumbBinding
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ThumbsFragment : Fragment(R.layout.fragment_thumbs_test) {
|
||||
|
||||
private var uri: String = ""
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val binding = FragmentThumbsTestBinding.bind(view)
|
||||
|
||||
val client = ViewModelFactory.getInstance(requireContext()).apiFactory.client
|
||||
binding.thumbList.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
|
||||
val thumbsAdapter = ThumbsAdapter()
|
||||
binding.thumbList.adapter = thumbsAdapter
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val list = ThumbnailParser(client).parse(uri)
|
||||
withContext(Dispatchers.Main) {
|
||||
thumbsAdapter.submitList(list)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val drawableMap: Map<String, Bitmap> = mutableMapOf()
|
||||
|
||||
private suspend fun loadImageForUri(uri: String): Drawable? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val imgUri = Uri.parse(uri)
|
||||
val sourceBitmap = drawableMap[imgUri.path] ?: Glide.with(requireContext())
|
||||
.asBitmap()
|
||||
.load(imgUri)
|
||||
.submit()
|
||||
.get()
|
||||
val params = imgUri.getQueryParameter("xywh")?.split(",")
|
||||
return@withContext if (params?.size == 4) {
|
||||
val (x, y, w, h) = params.map { it.toInt() }
|
||||
BitmapDrawable(Bitmap.createBitmap(sourceBitmap, x, y, w, h))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class ThumbsAdapter : ListAdapter<ThumbnailParser.ThumbnailInfo, ThumbsAdapter.ViewHolder>(object : DiffUtil.ItemCallback<ThumbnailParser.ThumbnailInfo>() {
|
||||
override fun areItemsTheSame(oldItem: ThumbnailParser.ThumbnailInfo, newItem: ThumbnailParser.ThumbnailInfo): Boolean = oldItem === newItem
|
||||
override fun areContentsTheSame(oldItem: ThumbnailParser.ThumbnailInfo, newItem: ThumbnailParser.ThumbnailInfo): Boolean = oldItem == newItem
|
||||
}) {
|
||||
|
||||
inner class ViewHolder(val binding: ItemThumbBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val binding = ItemThumbBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return ViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
this@ThumbsFragment.lifecycleScope.launch {
|
||||
val item = getItem(position)
|
||||
holder.binding.apply {
|
||||
thumbImage.setImageDrawable(loadImageForUri(item.thumb))
|
||||
startTime.text = item.startTime.toString()
|
||||
endTime.text = item.endTime.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
7
touch/src/main/res/layout/fragment_thumbs_test.xml
Normal file
7
touch/src/main/res/layout/fragment_thumbs_test.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.recyclerview.widget.RecyclerView android:id="@+id/thumb_list"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:listitem="@layout/item_thumb"
|
||||
xmlns:tools="http://schemas.android.com/tools" />
|
38
touch/src/main/res/layout/item_thumb.xml
Normal file
38
touch/src/main/res/layout/item_thumb.xml
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/thumb_image"
|
||||
android:layout_width="160dp"
|
||||
android:layout_height="90dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:srcCompat="@tools:sample/backgrounds/scenic" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/start_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="123"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/thumb_image"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/end_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="123"
|
||||
app:layout_constraintStart_toEndOf="@id/thumb_image"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
Loading…
Reference in a new issue