Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ data class KodiRequest(
data class KodiParams(
@param:Json(name = "item") val item: KodiItem? = null,
@param:Json(name = "properties") val properties: List<String>? = null,
@param:Json(name = "playerid") val playerId: Int? = null,
@param:Json(name = "subtitle") val subtitle: String? = null,
)

@JsonClass(generateAdapter = true)
Expand All @@ -35,6 +37,20 @@ data class KodiResponse(
@param:Json(name = "result") val result: String,
)

/** A single player currently active on Kodi, as returned by Player.GetActivePlayers */
@JsonClass(generateAdapter = true)
data class KodiPlayer(
@param:Json(name = "playerid") val playerId: Int,
@param:Json(name = "type") val type: String,
)

@JsonClass(generateAdapter = true)
data class KodiActivePlayersResponse(
@param:Json(name = "id") val id: Int,
@param:Json(name = "jsonrpc") val jsonrpc: String,
@param:Json(name = "result") val result: List<KodiPlayer>,
)

@JsonClass(generateAdapter = true)
data class KodiGenericResponse(
@param:Json(name = "id") val id: Int,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.github.livingwithhippos.unchained.data.remote

import com.github.livingwithhippos.unchained.data.model.KodiActivePlayersResponse
import com.github.livingwithhippos.unchained.data.model.KodiGenericResponse
import com.github.livingwithhippos.unchained.data.model.KodiRequest
import com.github.livingwithhippos.unchained.data.model.KodiResponse
Expand All @@ -23,4 +24,18 @@ interface KodiApi {
@Header("Authorization") auth: String? = null,
@Header("Content-Type") contentType: String = "application/json",
): Response<KodiGenericResponse>

@POST("jsonrpc")
suspend fun getActivePlayers(
@Body body: KodiRequest,
@Header("Authorization") auth: String? = null,
@Header("Content-Type") contentType: String = "application/json",
): Response<KodiActivePlayersResponse>

@POST("jsonrpc")
suspend fun addSubtitle(
@Body body: KodiRequest,
@Header("Authorization") auth: String? = null,
@Header("Content-Type") contentType: String = "application/json",
): Response<KodiResponse>
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.github.livingwithhippos.unchained.data.remote

import com.github.livingwithhippos.unchained.data.model.KodiActivePlayersResponse
import com.github.livingwithhippos.unchained.data.model.KodiGenericResponse
import com.github.livingwithhippos.unchained.data.model.KodiRequest
import com.github.livingwithhippos.unchained.data.model.KodiResponse
Expand All @@ -9,4 +10,11 @@ interface KodiApiHelper {
suspend fun openUrl(request: KodiRequest, auth: String?): Response<KodiResponse>

suspend fun getVolume(request: KodiRequest, auth: String?): Response<KodiGenericResponse>

suspend fun getActivePlayers(
request: KodiRequest,
auth: String?,
): Response<KodiActivePlayersResponse>

suspend fun addSubtitle(request: KodiRequest, auth: String?): Response<KodiResponse>
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.github.livingwithhippos.unchained.data.remote

import com.github.livingwithhippos.unchained.data.model.KodiActivePlayersResponse
import com.github.livingwithhippos.unchained.data.model.KodiGenericResponse
import com.github.livingwithhippos.unchained.data.model.KodiRequest
import com.github.livingwithhippos.unchained.data.model.KodiResponse
Expand All @@ -13,4 +14,14 @@ class KodiApiHelperImpl(private val kodiApi: KodiApi) : KodiApiHelper {
request: KodiRequest,
auth: String?,
): Response<KodiGenericResponse> = kodiApi.getVolume(request, auth)

override suspend fun getActivePlayers(
request: KodiRequest,
auth: String?,
): Response<KodiActivePlayersResponse> = kodiApi.getActivePlayers(request, auth)

override suspend fun addSubtitle(
request: KodiRequest,
auth: String?,
): Response<KodiResponse> = kodiApi.addSubtitle(request, auth)
}
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,68 @@ constructor(protoStore: ProtoStore, @param:ClassicClient private val client: OkH
}
}

/**
* Adds a subtitle to the video currently playing on Kodi, using Player.AddSubtitle. The active
* player id is fetched first via Player.GetActivePlayers, since Kodi needs it to know which
* player to attach the subtitle to.
*/
suspend fun addSubtitle(
address: String,
subtitleUrl: String,
username: String? = null,
password: String? = null,
): KodiResponse? {
try {
val kodiApiHelper: KodiApiHelper =
if (address.endsWith("/")) provideApiHelper(address)
else provideApiHelper("$address/")

val auth = encodeAuthentication(username, password)

val activePlayers =
safeApiCall(
call = {
kodiApiHelper.getActivePlayers(
request =
KodiRequest(method = "Player.GetActivePlayers", params = KodiParams()),
auth = auth,
)
},
errorMessage = "Error getting Kodi active players",
)

// prefer the video player, since that's the one a subtitle would be attached to
val playerId =
activePlayers?.result?.firstOrNull { it.type == "video" }?.playerId
?: activePlayers?.result?.firstOrNull()?.playerId

if (playerId == null) {
Timber.e("No active Kodi player found, can't add subtitle")
return null
}

val kodiResponse =
safeApiCall(
call = {
kodiApiHelper.addSubtitle(
request =
KodiRequest(
method = "Player.AddSubtitle",
params = KodiParams(playerId = playerId, subtitle = subtitleUrl),
),
auth = auth,
)
},
errorMessage = "Error adding subtitle on Kodi",
)

return kodiResponse
} catch (e: Exception) {
Timber.e(e)
return null
}
}

private fun encodeAuthentication(username: String?, password: String?): String? {
return if (!username.isNullOrBlank() && !password.isNullOrBlank()) {
"Basic " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import com.github.livingwithhippos.unchained.utilities.extension.getAvailableSpa
import com.github.livingwithhippos.unchained.utilities.extension.getFileSizeString
import com.github.livingwithhippos.unchained.utilities.extension.openExternalWebPage
import com.github.livingwithhippos.unchained.utilities.extension.showToast
import com.github.livingwithhippos.unchained.utilities.extensionIconMap
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import timber.log.Timber
Expand Down Expand Up @@ -207,6 +208,16 @@ class DownloadDetailsFragment : UnchainedFragment(), DownloadDetailsListener {

binding.fabPickStreaming.setOnClickListener { popView -> manageStreamingPopup(popView) }

// a subtitle file can be attached to whatever is currently playing on Kodi, regardless of
// whether it's "streamable" on its own
val fileExtension = args.details.filename.substringAfterLast('.', "").lowercase()
if (extensionIconMap[fileExtension] == R.drawable.icon_subtitles) {
binding.llFabAddSubtitle.visibility = View.VISIBLE
binding.fabAddSubtitle.setOnClickListener { showAddSubtitleDialog() }
} else {
binding.llFabAddSubtitle.visibility = View.GONE
}

viewModel.streamLiveData.observe(viewLifecycleOwner) {
if (it != null) {
binding.fabLoadStreams.isEnabled = false
Expand Down Expand Up @@ -508,6 +519,15 @@ class DownloadDetailsFragment : UnchainedFragment(), DownloadDetailsListener {
}
}

private fun showAddSubtitleDialog() {
val dialog = ServicePickerDialog()
val bundle = Bundle()
bundle.putString("downloadUrl", args.details.download)
bundle.putBoolean("addSubtitle", true)
dialog.arguments = bundle
dialog.show(parentFragmentManager, "ServicePickerDialog")
}

private fun playOnService(
link: String,
service: CompleteRemoteService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.RecyclerView
import com.github.livingwithhippos.unchained.R
import com.github.livingwithhippos.unchained.data.local.CompleteRemoteServiceDetails
import com.github.livingwithhippos.unchained.data.local.RemoteServiceType
import com.github.livingwithhippos.unchained.data.local.serviceTypeMap
import com.github.livingwithhippos.unchained.downloaddetails.model.ServicePickerAdapter
import com.github.livingwithhippos.unchained.downloaddetails.model.ServicePickerListener
Expand All @@ -22,6 +23,11 @@ class ServicePickerDialog : DialogFragment(), ServicePickerListener {

private val viewModel: DownloadDetailsViewModel by activityViewModels()

// when true, the picked service is used to add the link as a subtitle to whatever is
// currently playing on Kodi, instead of opening it as a new video
private val addSubtitleMode: Boolean
get() = arguments?.getBoolean("addSubtitle") ?: false

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let { activity ->

Expand All @@ -42,12 +48,16 @@ class ServicePickerDialog : DialogFragment(), ServicePickerListener {
is DownloadEvent.AllServices -> {

val devSer: List<CompleteRemoteServiceDetails> =
content.services.map { serv ->
CompleteRemoteServiceDetails(
service = serv,
type = serviceTypeMap[serv.type]!!,
)
}
content.services
// adding a subtitle to the currently playing video is a Kodi-only
// feature, so only Kodi services make sense here
.filter { serv -> !addSubtitleMode || serv.type == RemoteServiceType.KODI.value }
.map { serv ->
CompleteRemoteServiceDetails(
service = serv,
type = serviceTypeMap[serv.type]!!,
)
}
adapter.submitList(devSer)
}
else -> {}
Expand All @@ -71,6 +81,13 @@ class ServicePickerDialog : DialogFragment(), ServicePickerListener {
if (link == null) {
Timber.e("Download url is null")
context?.showToast(R.string.error)
} else if (addSubtitleMode) {
if (serviceDetails.type == RemoteServiceType.KODI) {
viewModel.addSubtitleOnKodi(link, serviceDetails.service)
} else {
Timber.e("Adding a subtitle is only supported on Kodi")
context?.showToast(R.string.error)
}
} else {
viewModel.openOnRemoteService(serviceDetails, link)
// show toast?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,25 @@ constructor(
}
}

fun addSubtitleOnKodi(subtitleURL: String, kodiService: CompleteRemoteService) {
viewModelScope.launch {
try {
val response =
kodiRepository.addSubtitle(
kodiService.address,
subtitleURL,
kodiService.username,
kodiService.password,
)
if (response != null) messageLiveData.postEvent(DownloadDetailsMessage.KodiSuccess)
else messageLiveData.postEvent(DownloadDetailsMessage.KodiError)
} catch (e: Exception) {
Timber.e("Error adding subtitle on Kodi: ${e.message}")
messageLiveData.postEvent(DownloadDetailsMessage.KodiError)
}
}
}

fun openUrlOnVLC(mediaURL: String, vlcService: CompleteRemoteService) {

viewModelScope.launch {
Expand Down
27 changes: 27 additions & 0 deletions app/app/src/main/res/layout/fragment_download_details.xml
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,33 @@
android:textAppearance="?attr/textAppearanceLabelSmall" />
</LinearLayout>

<LinearLayout
android:id="@+id/llFabAddSubtitle"
android:layout_width="@dimen/fab_download_details_width"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">

<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabAddSubtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/add_subtitle_to_kodi"
app:srcCompat="@drawable/icon_subtitles" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:focusable="false"
android:text="@string/add_subtitle_to_kodi"
android:textAlignment="center"
android:textAppearance="?attr/textAppearanceLabelSmall" />
</LinearLayout>

<LinearLayout
android:id="@+id/llFabLoadStreams"
android:layout_width="@dimen/fab_download_details_width"
Expand Down
1 change: 1 addition & 0 deletions app/app/src/main/res/values-es/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@
<string name="save_device">Guardar dispositivo</string>
<string name="update_device">Actualizar el dispositivo</string>
<string name="kodi_missing_default">El dispositivo Kodi por defecto no está configurado</string>
<string name="add_subtitle_to_kodi">Añadir subtítulos a Kodi</string>
<string name="device_deleted">Dispositivo eliminado</string>
<string name="ui_settings">Configuración de la interfaz de usuario</string>
<string name="show_media_player_button_summary">Mostrar el botón para enviar los medios a un reproductor instalado</string>
Expand Down
1 change: 1 addition & 0 deletions app/app/src/main/res/values-fr/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@
<string name="save_device">Sauver le dispositif</string>
<string name="update_device">Mettre à jour le dispositif</string>
<string name="kodi_missing_default">Le périphérique Kodi par défaut n\'est pas défini</string>
<string name="add_subtitle_to_kodi">Ajouter des sous-titres à Kodi</string>
<string name="device_deleted">Dispositif supprimé</string>
<string name="ui_settings">Paramètres de l\'IU</string>
<string name="show_media_player_button_summary">Afficher le bouton pour envoyer le média vers un lecteur installé</string>
Expand Down
1 change: 1 addition & 0 deletions app/app/src/main/res/values-it/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@
<string name="save_device">Salva dispositivo</string>
<string name="update_device">Aggiorna dispositivo</string>
<string name="kodi_missing_default">Il dIspositivo Kodi di default non è impostato</string>
<string name="add_subtitle_to_kodi">Aggiungi sottotitoli a Kodi</string>
<string name="device_deleted">Dispositivo eliminato</string>
<string name="ui_settings">Impostazioni UI</string>
<string name="show_media_player_button_summary">Mostra un bottone per inviare media ad un lettore locale</string>
Expand Down
1 change: 1 addition & 0 deletions app/app/src/main/res/values-ko/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@
<string name="save_device">기기 저장</string>
<string name="update_device">기기 업데이트</string>
<string name="kodi_missing_default">기본 Kodi 기기가 설정되지 않았습니다</string>
<string name="add_subtitle_to_kodi">Kodi에 자막 추가</string>
<string name="device_deleted">기기 삭제됨</string>
<string name="ui_settings">UI 설정</string>
<string name="show_media_player_button_summary">설치된 플레이어로 미디어를 보내는 버튼 표시</string>
Expand Down
1 change: 1 addition & 0 deletions app/app/src/main/res/values-tr/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@
<string name="save_device">Cihazı Kaydet</string>
<string name="update_device">Cihazı Güncelle</string>
<string name="kodi_missing_default">Varsayılan Kodi cihazı ayarlanmadı</string>
<string name="add_subtitle_to_kodi">Kodi\'ye altyazı ekle</string>
<string name="device_deleted">Cihaz silindi</string>
<string name="ui_settings">Kullanıcı Arayüzü Ayarları</string>
<string name="show_media_player_button_summary">Medyayı yüklü bir oynatıcıya göndermek için düğmeyi göster</string>
Expand Down
1 change: 1 addition & 0 deletions app/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,7 @@
<string name="save_device">Save Device</string>
<string name="update_device">Update Device</string>
<string name="kodi_missing_default">The default Kodi device is not set</string>
<string name="add_subtitle_to_kodi">Add subtitle to Kodi</string>
<string name="device_deleted">Device deleted</string>
<string name="ui_settings">UI Settings</string>
<string name="player_vlc" translatable="false">VLC</string>
Expand Down