Subtitle download using the new API

This commit is contained in:
Nicolas Pomepuy 2024-10-29 12:52:09 +01:00 committed by Duncan McNamara
parent d2aa35e233
commit 7f09ad5715
9 changed files with 95 additions and 69 deletions

View File

@ -1,7 +1,9 @@
package org.videolan.resources.opensubtitles
import org.videolan.resources.opensubtitles.OpenSubV1
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query
interface IOpenSubtitleService {
@ -16,6 +18,9 @@ interface IOpenSubtitleService {
@Query("season_number") season: Int? = null,
): OpenSubV1
@POST("download")
suspend fun queryDownloadUrl( @Body downloadLinkBody: DownloadLinkBody): Response<DownloadLink>
}

View File

@ -1,11 +1,12 @@
package org.videolan.resources.opensubtitles
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
data class QueryParameters(
@field:Json(name = "query") val query: String,
@field:Json(name = "episode") val episode: String,
@field:Json(name = "season") val season: String
@field:Json(name = "query") val query: String,
@field:Json(name = "episode") val episode: String,
@field:Json(name = "season") val season: String
)
@ -123,7 +124,7 @@ data class File(
@field:Json(name = "cd_number")
val cdNumber: Int?,
@field:Json(name = "file_id")
val fileId: Int,
val fileId: Long,
@field:Json(name = "file_name")
val fileName: String
)
@ -148,3 +149,25 @@ data class Uploader(
val uploaderId: Int?
)
data class DownloadLink(
@field:Json(name = "file_name")
val fileName: String,
@field:Json(name = "link")
val link: String,
@field:Json(name = "message")
val message: String,
@field:Json(name = "remaining")
val remaining: Int,
@field:Json(name = "requests")
val requests: Int,
@field:Json(name = "reset_time")
val resetTime: String,
@field:Json(name = "reset_time_utc")
val resetTimeUtc: String
)
data class DownloadLinkBody(
@field:Json(name = "file_id")
val fileId: Long,
)

View File

@ -40,6 +40,11 @@ class OpenSubtitleRepository(private val openSubtitleService: IOpenSubtitleServi
}
suspend fun getDownloadLink(fileId: Long): DownloadLink {
val query = openSubtitleService.queryDownloadUrl(DownloadLinkBody(fileId))
return query.body() ?: throw Exception("No body")
}
companion object {
// To ensure the instance can be overridden in tests.
var instance = lazy { OpenSubtitleRepository(OpenSubtitleClient.instance) }

View File

@ -17,7 +17,7 @@ import java.util.concurrent.TimeUnit
private const val BASE_URL = "https://api.opensubtitles.com/api/v1/"
private const val USER_AGENT = "VLSub v0.9"
const val USER_AGENT = "VLSub v0.9"
private fun buildClient() = Retrofit.Builder()
.baseUrl(BASE_URL)
@ -46,6 +46,7 @@ private class UserAgentInterceptor(val userAgent: String): Interceptor {
val userAgentRequest: Request = request.newBuilder()
.header("User-Agent", userAgent)
.header("Api-Key", BuildConfig.VLC_OPEN_SUBTITLES_API_KEY)
.header("Accept", "application/json")
.build()
return chain.proceed(userAgentRequest)
}
@ -54,6 +55,5 @@ private class UserAgentInterceptor(val userAgent: String): Interceptor {
interface OpenSubtitleClient {
companion object {
val instance: IOpenSubtitleService by lazy { buildClient() }
fun getDownloadLink(id:Int) = "${BASE_URL}download?file_id=$id"
}
}

View File

@ -4,7 +4,11 @@ import android.content.DialogInterface
import android.content.res.Configuration
import android.net.Uri
import android.os.Bundle
import android.view.*
import android.view.Gravity
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.Toast
import androidx.annotation.StringRes
@ -14,9 +18,12 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ObsoleteCoroutinesApi
import kotlinx.coroutines.channels.actor
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import org.videolan.resources.opensubtitles.OpenSubtitleRepository
import org.videolan.resources.util.parcelableList
import org.videolan.vlc.R
import org.videolan.vlc.databinding.SubtitleDownloaderDialogBinding
@ -56,7 +63,15 @@ class SubtitleDownloaderDialogFragment : VLCBottomSheetDialogFragment() {
private val listEventActor = lifecycleScope.actor<SubtitleEvent> {
for (subtitleEvent in channel) if (isActive) when (subtitleEvent) {
is SubtitleClick -> when (subtitleEvent.item.state) {
State.NotDownloaded -> VLCDownloadManager.download(requireActivity(), subtitleEvent.item)
State.NotDownloaded -> {
withContext(Dispatchers.IO) {
val downloadLink = OpenSubtitleRepository.getInstance()
.getDownloadLink(subtitleEvent.item.fileId)
subtitleEvent.item.zipDownloadLink = downloadLink.link
subtitleEvent.item.fileName = downloadLink.fileName
}
VLCDownloadManager.download(requireActivity(), subtitleEvent.item, true)
}
State.Downloaded -> deleteSubtitleDialog(requireActivity(), DialogInterface.OnClickListener { _, _ ->
subtitleEvent.item.mediaUri.path?.let { viewModel.deleteSubtitle(it, subtitleEvent.item.idSubtitle) }
}
@ -153,7 +168,7 @@ class SubtitleDownloaderDialogFragment : VLCBottomSheetDialogFragment() {
}
})
//todo
viewModel.observableSearchHearingImpaired.set(true)
viewModel.observableSearchHearingImpaired.set(false)
binding.retryButton.setOnClickListener {
viewModel.onRefresh()

View File

@ -5,14 +5,16 @@ import org.videolan.tools.readableNumber
data class SubtitleItem(
val idSubtitle: String,
val fileId: Long,
val mediaUri: Uri,
val subLanguageID: String,
val movieReleaseName: String,
val state: State,
val zipDownloadLink: String,
var zipDownloadLink: String,
val hearingImpaired: Boolean,
val rating: Int,
val downloadNumber: Long
val downloadNumber: Long,
var fileName: String = ""
) {
fun getReadableDownloadNumber() = downloadNumber.readableNumber()
}

View File

@ -194,6 +194,14 @@ object FileUtils {
}
}
@WorkerThread
fun copyFile(src: String, dst: String): String? {
return if (copyFile(File(src), File(dst)))
dst
else
null
}
@WorkerThread
fun copyFile(src: File, dst: File): Boolean {
var ret = true
@ -217,7 +225,8 @@ object FileUtils {
len = inputStream.read(buf)
}
return true
} catch (ignored: IOException) {
} catch (exception: IOException) {
Log.e(TAG, exception.message, exception)
} finally {
CloseableUtils.close(inputStream)
CloseableUtils.close(out)
@ -453,51 +462,6 @@ object FileUtils {
return volumeDescription
}
suspend fun unpackZip(path: String, unzipDirectory: String): ArrayList<String> = withContext(Dispatchers.IO) {
val fis: InputStream
val zis: ZipInputStream
val unzippedFiles = ArrayList<String>()
File(unzipDirectory).mkdirs()
try {
fis = FileInputStream(path)
zis = ZipInputStream(BufferedInputStream(fis))
var ze = zis.nextEntry
while (ze != null) {
val baos = ByteArrayOutputStream()
val buffer = ByteArray(1024)
var count = zis.read(buffer)
val filename = ze.name.replace('/', ' ')
if (filename.endsWith(".nfo")) {
zis.closeEntry()
ze = zis.nextEntry
continue
}
val fileToUnzip = File(unzipDirectory, filename)
val fout = FileOutputStream(fileToUnzip)
// reading and writing
while (count != -1) {
baos.write(buffer, 0, count)
val bytes = baos.toByteArray()
fout.write(bytes)
baos.reset()
count = zis.read(buffer)
}
unzippedFiles.add(fileToUnzip.absolutePath)
fout.close()
zis.closeEntry()
ze = zis.nextEntry
}
zis.close()
} catch (e: IOException) {
e.printStackTrace()
}
unzippedFiles
}
const val BUFFER = 2048
fun zip(files: Array<String>, zipFileName: String):Boolean {
return try {

View File

@ -5,6 +5,7 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.util.Log
import android.widget.Toast
import androidx.core.content.getSystemService
import androidx.core.net.toUri
@ -17,12 +18,15 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.videolan.libvlc.util.Extensions
import org.videolan.resources.AppContextProvider
import org.videolan.resources.opensubtitles.USER_AGENT
import org.videolan.resources.util.registerReceiverCompat
import org.videolan.tools.isStarted
import org.videolan.vlc.BuildConfig
import org.videolan.vlc.R
import org.videolan.vlc.gui.dialogs.SubtitleItem
import org.videolan.vlc.gui.helpers.hf.getExtWritePermission
import org.videolan.vlc.repository.ExternalSubRepository
import java.io.File
object VLCDownloadManager: BroadcastReceiver(), DefaultLifecycleObserver {
@ -62,10 +66,12 @@ object VLCDownloadManager: BroadcastReceiver(), DefaultLifecycleObserver {
AppContextProvider.appContext.applicationContext.unregisterReceiver(this)
}
suspend fun download(context: FragmentActivity, subtitleItem: SubtitleItem) {
suspend fun download(context: FragmentActivity, subtitleItem: SubtitleItem, useOpenSubtitlesHeader: Boolean = false) {
val request = DownloadManager.Request(subtitleItem.zipDownloadLink.toUri())
request.setDescription(subtitleItem.movieReleaseName)
request.setTitle(context.resources.getString(R.string.download_subtitle_title))
if (useOpenSubtitlesHeader) request.addRequestHeader("User-Agent", USER_AGENT)
if (useOpenSubtitlesHeader) request.addRequestHeader("Api-Key", org.videolan.resources.BuildConfig.VLC_OPEN_SUBTITLES_API_KEY)
request.setDestinationInExternalFilesDir(context, getDownloadPath(subtitleItem), "")
val id = downloadManager.enqueue(request)
val deferred = CompletableDeferred<SubDlResult>().also { dlDeferred = it }
@ -78,14 +84,13 @@ object VLCDownloadManager: BroadcastReceiver(), DefaultLifecycleObserver {
private suspend fun downloadSuccessful(id:Long, subtitleItem: SubtitleItem, localUri: String, context: FragmentActivity) {
val extractDirectory = getFinalDirectory(context, subtitleItem) ?: return
val downloadedPaths = FileUtils.unpackZip(localUri, extractDirectory)
subtitleItem.run {
ExternalSubRepository.getInstance(context).removeDownloadingItem(id)
downloadedPaths.forEach {
if (Extensions.SUBTITLES.contains(".${it.split('.').last()}")) {
FileUtils.copyFile(localUri, "$extractDirectory/${subtitleItem.fileName}")?.let {dest ->
subtitleItem.run {
ExternalSubRepository.getInstance(context).removeDownloadingItem(id)
if (Extensions.SUBTITLES.contains(".${dest.split('.').last()}")) {
ExternalSubRepository.getInstance(context).saveDownloadedSubtitle(
idSubtitle,
it,
dest,
mediaUri.path!!,
subLanguageID,
movieReleaseName,
@ -94,7 +99,8 @@ object VLCDownloadManager: BroadcastReceiver(), DefaultLifecycleObserver {
}
else
Toast.makeText(context, R.string.subtitles_download_failed, Toast.LENGTH_SHORT).show()
}
}
withContext(Dispatchers.IO) { FileUtils.deleteFile(localUri) }
}
}
@ -113,13 +119,18 @@ object VLCDownloadManager: BroadcastReceiver(), DefaultLifecycleObserver {
ExternalSubRepository.getInstance(context).removeDownloadingItem(id)
}
private fun getDownloadPath(subtitleItem: SubtitleItem) = "VLC/${subtitleItem.movieReleaseName}_${subtitleItem.idSubtitle}.zip"
private fun getDownloadPath(subtitleItem: SubtitleItem) = "VLC/${subtitleItem.movieReleaseName}_${subtitleItem.fileName}.zip"
private fun getDownloadState(downloadId: Long): Pair<Int, String> {
val query = DownloadManager.Query()
query.setFilterById(downloadId)
val cursor = downloadManager.query(query)
cursor.moveToFirst()
val reasonIndex = cursor.getColumnIndex(DownloadManager.COLUMN_REASON)
val reason = cursor.getInt(reasonIndex)
if (BuildConfig.DEBUG) Log.d("VLCDownloadManager", "Reason: $reason")
val statusIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
val status = if (statusIndex != -1)

View File

@ -67,7 +67,7 @@ class SubtitlesModel(private val context: Context, private val mediaUri: Uri, pr
private val apiResultLiveData: MutableLiveData<List<Data>> = MutableLiveData()
private val downloadedLiveData = ExternalSubRepository.getInstance(context).getDownloadedSubtitles(mediaUri).map { list ->
list.map { SubtitleItem(it.idSubtitle, mediaUri, it.subLanguageID, it.movieReleaseName, State.Downloaded, "", it.hearingImpaired, 0, 0) }
list.map { SubtitleItem(it.idSubtitle, -1, mediaUri, it.subLanguageID, it.movieReleaseName, State.Downloaded, "", it.hearingImpaired, 0, 0) }
}
private val downloadingLiveData = ExternalSubRepository.getInstance(context).downloadingSubtitles
@ -130,11 +130,12 @@ class SubtitlesModel(private val context: Context, private val mediaUri: Uri, pr
list.add(
SubtitleItem(
openSubtitle.attributes.subtitleId,
openSubtitle.attributes.files.first().fileId,
mediaUri,
openSubtitle.attributes.language,
openSubtitle.attributes.featureDetails.title,
state,
OpenSubtitleClient.getDownloadLink(openSubtitle.attributes.files.first().fileId),
"",
openSubtitle.attributes.hearingImpaired,
openSubtitle.attributes.ratings,
openSubtitle.attributes.downloadCount