Phase 2: network scaffold (bookshelf-api + ABS clients, DI, models)
This commit is contained in:
parent
ad821faecd
commit
2b63ff2bec
13 changed files with 592 additions and 0 deletions
|
|
@ -164,4 +164,19 @@ dependencies {
|
||||||
|
|
||||||
// Json
|
// Json
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
||||||
|
|
||||||
|
// Network (bookshelf-api + ABS)
|
||||||
|
implementation("com.squareup.retrofit2:retrofit:2.11.0")
|
||||||
|
implementation("com.squareup.retrofit2:converter-kotlinx-serialization:2.11.0")
|
||||||
|
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||||
|
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
|
||||||
|
|
||||||
|
// Audio (Media3 / ExoPlayer)
|
||||||
|
implementation("androidx.media3:media3-exoplayer:1.6.0")
|
||||||
|
implementation("androidx.media3:media3-session:1.6.0")
|
||||||
|
implementation("androidx.media3:media3-ui:1.6.0")
|
||||||
|
implementation("androidx.media3:media3-datasource-okhttp:1.6.0")
|
||||||
|
|
||||||
|
// Background work (cache, TTS sync)
|
||||||
|
implementation("androidx.work:work-runtime-ktx:2.10.0")
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package org.dueattendant149.bookshelf.data.di
|
||||||
|
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object NetworkModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideJson(): Json = Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
explicitNulls = false
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideOkHttpClient(): OkHttpClient =
|
||||||
|
OkHttpClient
|
||||||
|
.Builder()
|
||||||
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(60, TimeUnit.SECONDS)
|
||||||
|
.writeTimeout(60, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
package org.dueattendant149.bookshelf.data.remote.audiobookshelf
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
import org.dueattendant149.bookshelf.domain.util.fixUriScheme
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds (and caches) a Retrofit-backed [AudiobookshelfApiService] for the configured
|
||||||
|
* Audiobookshelf instance. All requests are authenticated with the provided bearer token.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class AudiobookshelfApiClientFactory
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
private val json: Json,
|
||||||
|
private val okHttpClient: OkHttpClient,
|
||||||
|
) {
|
||||||
|
private var cachedUrl: String? = null
|
||||||
|
private var cachedToken: String? = null
|
||||||
|
private var cachedClient: AudiobookshelfApiService? = null
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun provideClient(
|
||||||
|
url: String,
|
||||||
|
token: String,
|
||||||
|
): AudiobookshelfApiService? {
|
||||||
|
val fixedUrl = url.fixUriScheme()
|
||||||
|
if (fixedUrl == cachedUrl && token == cachedToken && cachedClient != null) {
|
||||||
|
return cachedClient
|
||||||
|
}
|
||||||
|
|
||||||
|
val authClient =
|
||||||
|
okHttpClient
|
||||||
|
.newBuilder()
|
||||||
|
.addInterceptor { chain ->
|
||||||
|
val request =
|
||||||
|
chain
|
||||||
|
.request()
|
||||||
|
.newBuilder()
|
||||||
|
.header("Authorization", "Bearer $token")
|
||||||
|
.build()
|
||||||
|
chain.proceed(request)
|
||||||
|
}
|
||||||
|
.addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BASIC })
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return runCatching {
|
||||||
|
Retrofit
|
||||||
|
.Builder()
|
||||||
|
.client(authClient)
|
||||||
|
.baseUrl(fixedUrl)
|
||||||
|
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
|
||||||
|
.build()
|
||||||
|
.create(AudiobookshelfApiService::class.java)
|
||||||
|
}.onFailure {
|
||||||
|
Log.e("AudiobookshelfApiFactory", "Failed to create client for $fixedUrl", it)
|
||||||
|
}.getOrNull().also {
|
||||||
|
cachedUrl = fixedUrl
|
||||||
|
cachedToken = token
|
||||||
|
cachedClient = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearCache() {
|
||||||
|
cachedUrl = null
|
||||||
|
cachedToken = null
|
||||||
|
cachedClient = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package org.dueattendant149.bookshelf.data.remote.audiobookshelf
|
||||||
|
|
||||||
|
import okhttp3.ResponseBody
|
||||||
|
import retrofit2.Response
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Path
|
||||||
|
import retrofit2.http.Streaming
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Direct Audiobookshelf API calls for audio files and covers.
|
||||||
|
* bookshelf-api does not proxy audio streams, so the app talks to ABS directly.
|
||||||
|
*/
|
||||||
|
interface AudiobookshelfApiService {
|
||||||
|
|
||||||
|
@Streaming
|
||||||
|
@GET("api/items/{itemId}/file/{fileId}")
|
||||||
|
suspend fun downloadAudioFile(
|
||||||
|
@Path("itemId") itemId: String,
|
||||||
|
@Path("fileId") fileId: String,
|
||||||
|
): Response<ResponseBody>
|
||||||
|
|
||||||
|
@Streaming
|
||||||
|
@GET("api/items/{itemId}/download")
|
||||||
|
suspend fun downloadBook(
|
||||||
|
@Path("itemId") itemId: String,
|
||||||
|
): Response<ResponseBody>
|
||||||
|
|
||||||
|
@GET("api/items/{itemId}/cover")
|
||||||
|
suspend fun getCover(
|
||||||
|
@Path("itemId") itemId: String,
|
||||||
|
): Response<ResponseBody>
|
||||||
|
|
||||||
|
@GET("api/me/progress/{itemId}")
|
||||||
|
suspend fun getProgress(
|
||||||
|
@Path("itemId") itemId: String,
|
||||||
|
): Response<String>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
package org.dueattendant149.bookshelf.data.remote.bookshelfapi
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
import org.dueattendant149.bookshelf.domain.util.fixUriScheme
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds (and caches) a Retrofit-backed [BookshelfApiService] for the configured
|
||||||
|
* bookshelf-api instance.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class BookshelfApiClientFactory
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
private val json: Json,
|
||||||
|
private val okHttpClient: OkHttpClient,
|
||||||
|
) {
|
||||||
|
private var cachedUrl: String? = null
|
||||||
|
private var cachedClient: BookshelfApiService? = null
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun provideClient(url: String): BookshelfApiService? {
|
||||||
|
val fixedUrl = url.fixUriScheme()
|
||||||
|
if (fixedUrl == cachedUrl && cachedClient != null) {
|
||||||
|
return cachedClient
|
||||||
|
}
|
||||||
|
|
||||||
|
return runCatching {
|
||||||
|
Retrofit
|
||||||
|
.Builder()
|
||||||
|
.client(
|
||||||
|
okHttpClient
|
||||||
|
.newBuilder()
|
||||||
|
.addInterceptor(HttpLoggingInterceptor().apply {
|
||||||
|
level = HttpLoggingInterceptor.Level.BASIC
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.baseUrl(fixedUrl)
|
||||||
|
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
|
||||||
|
.build()
|
||||||
|
.create(BookshelfApiService::class.java)
|
||||||
|
}.onFailure {
|
||||||
|
Log.e("BookshelfApiClientFactory", "Failed to create BookshelfApiService for $fixedUrl", it)
|
||||||
|
}.getOrNull().also {
|
||||||
|
cachedUrl = fixedUrl
|
||||||
|
cachedClient = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearCache() {
|
||||||
|
cachedUrl = null
|
||||||
|
cachedClient = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
package org.dueattendant149.bookshelf.data.remote.bookshelfapi
|
||||||
|
|
||||||
|
import okhttp3.ResponseBody
|
||||||
|
import org.dueattendant149.bookshelf.data.remote.bookshelfapi.model.BookItemResponse
|
||||||
|
import org.dueattendant149.bookshelf.data.remote.bookshelfapi.model.DownloadRequest
|
||||||
|
import org.dueattendant149.bookshelf.data.remote.bookshelfapi.model.DownloadResponse
|
||||||
|
import org.dueattendant149.bookshelf.data.remote.bookshelfapi.model.DownloadsListResponse
|
||||||
|
import org.dueattendant149.bookshelf.data.remote.bookshelfapi.model.LibraryResponse
|
||||||
|
import org.dueattendant149.bookshelf.data.remote.bookshelfapi.model.PodcastRequest
|
||||||
|
import org.dueattendant149.bookshelf.data.remote.bookshelfapi.model.PodcastResponse
|
||||||
|
import org.dueattendant149.bookshelf.data.remote.bookshelfapi.model.SearchRequest
|
||||||
|
import org.dueattendant149.bookshelf.data.remote.bookshelfapi.model.SearchResponse
|
||||||
|
import org.dueattendant149.bookshelf.data.remote.bookshelfapi.model.TtsCreateRequest
|
||||||
|
import org.dueattendant149.bookshelf.data.remote.bookshelfapi.model.TtsEnginesResponse
|
||||||
|
import org.dueattendant149.bookshelf.data.remote.bookshelfapi.model.TtsJobResponse
|
||||||
|
import org.dueattendant149.bookshelf.data.remote.bookshelfapi.model.TtsJobsResponse
|
||||||
|
import org.dueattendant149.bookshelf.data.remote.bookshelfapi.model.TtsVoicesResponse
|
||||||
|
import org.dueattendant149.bookshelf.data.remote.bookshelfapi.model.YandexRequest
|
||||||
|
import org.dueattendant149.bookshelf.data.remote.bookshelfapi.model.YandexResponse
|
||||||
|
import retrofit2.Response
|
||||||
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.Path
|
||||||
|
import retrofit2.http.Query
|
||||||
|
import retrofit2.http.Streaming
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrofit description of the Bookshelf API (bookshelf-api:8073).
|
||||||
|
* All endpoints are relative to the configured base URL.
|
||||||
|
*/
|
||||||
|
interface BookshelfApiService {
|
||||||
|
|
||||||
|
// Libraries / books
|
||||||
|
@GET("api/v1/books/libraries")
|
||||||
|
suspend fun getLibraries(): Response<List<LibraryResponse>>
|
||||||
|
|
||||||
|
@GET("api/v1/books/{itemId}")
|
||||||
|
suspend fun getBook(
|
||||||
|
@Path("itemId") itemId: String,
|
||||||
|
): Response<String>
|
||||||
|
|
||||||
|
@GET("api/v1/books/library/{libraryId}/search")
|
||||||
|
suspend fun searchLibrary(
|
||||||
|
@Path("libraryId") libraryId: String,
|
||||||
|
@Query("q") query: String,
|
||||||
|
): Response<List<BookItemResponse>>
|
||||||
|
|
||||||
|
@POST("api/v1/search")
|
||||||
|
suspend fun search(
|
||||||
|
@Body request: SearchRequest,
|
||||||
|
): Response<SearchResponse>
|
||||||
|
|
||||||
|
// TTS
|
||||||
|
@GET("api/v1/tts/engines")
|
||||||
|
suspend fun getTtsEngines(): Response<TtsEnginesResponse>
|
||||||
|
|
||||||
|
@GET("api/v1/tts/voices")
|
||||||
|
suspend fun getTtsVoices(
|
||||||
|
@Query("engine") engine: String? = null,
|
||||||
|
): Response<TtsVoicesResponse>
|
||||||
|
|
||||||
|
@POST("api/v1/tts")
|
||||||
|
suspend fun createTtsJob(
|
||||||
|
@Body request: TtsCreateRequest,
|
||||||
|
): Response<TtsJobResponse>
|
||||||
|
|
||||||
|
@GET("api/v1/tts/{jobId}")
|
||||||
|
suspend fun getTtsJobStatus(
|
||||||
|
@Path("jobId") jobId: String,
|
||||||
|
): Response<TtsJobResponse>
|
||||||
|
|
||||||
|
@GET("api/v1/tts/jobs/list")
|
||||||
|
suspend fun listTtsJobs(): Response<TtsJobsResponse>
|
||||||
|
|
||||||
|
@Streaming
|
||||||
|
@GET("api/v1/tts/{jobId}/download")
|
||||||
|
suspend fun downloadTtsAudio(
|
||||||
|
@Path("jobId") jobId: String,
|
||||||
|
): Response<ResponseBody>
|
||||||
|
|
||||||
|
// Downloads / sources
|
||||||
|
@POST("api/v1/download")
|
||||||
|
suspend fun startDownload(
|
||||||
|
@Body request: DownloadRequest,
|
||||||
|
): Response<DownloadResponse>
|
||||||
|
|
||||||
|
@GET("api/v1/download/list")
|
||||||
|
suspend fun listDownloads(): Response<DownloadsListResponse>
|
||||||
|
|
||||||
|
@POST("api/v1/podcasts")
|
||||||
|
suspend fun downloadPodcast(
|
||||||
|
@Body request: PodcastRequest,
|
||||||
|
): Response<PodcastResponse>
|
||||||
|
|
||||||
|
@POST("api/v1/sources/yandex")
|
||||||
|
suspend fun downloadYandex(
|
||||||
|
@Body request: YandexRequest,
|
||||||
|
): Response<YandexResponse>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package org.dueattendant149.bookshelf.data.remote.bookshelfapi.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BookItemResponse(
|
||||||
|
val id: String,
|
||||||
|
val title: String = "",
|
||||||
|
val author: String = "",
|
||||||
|
@SerialName("media_type")
|
||||||
|
val mediaType: String = "",
|
||||||
|
@SerialName("library_id")
|
||||||
|
val libraryId: String = "",
|
||||||
|
@SerialName("cover_url")
|
||||||
|
val coverUrl: String = "",
|
||||||
|
val duration: Double = 0.0,
|
||||||
|
val size: Long = 0L,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
package org.dueattendant149.bookshelf.data.remote.bookshelfapi.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class DownloadRequest(
|
||||||
|
val source: String = "",
|
||||||
|
val title: String = "",
|
||||||
|
val author: String = "",
|
||||||
|
@SerialName("download_url")
|
||||||
|
val downloadUrl: String? = null,
|
||||||
|
@SerialName("magnet_url")
|
||||||
|
val magnetUrl: String? = null,
|
||||||
|
@SerialName("info_hash")
|
||||||
|
val infoHash: String? = null,
|
||||||
|
val md5: String? = null,
|
||||||
|
val url: String? = null,
|
||||||
|
@SerialName("media_type")
|
||||||
|
val mediaType: String = "ebook",
|
||||||
|
@SerialName("download_protocol")
|
||||||
|
val downloadProtocol: String = "",
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class DownloadResponse(
|
||||||
|
val success: Boolean = false,
|
||||||
|
val error: String? = null,
|
||||||
|
@SerialName("download_id")
|
||||||
|
val downloadId: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class DownloadStatusResponse(
|
||||||
|
val title: String = "",
|
||||||
|
val status: String = "",
|
||||||
|
val progress: Double = 0.0,
|
||||||
|
val speed: String = "",
|
||||||
|
val error: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class DownloadsListResponse(
|
||||||
|
val downloads: List<DownloadStatusResponse> = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PodcastRequest(
|
||||||
|
val url: String,
|
||||||
|
val title: String? = null,
|
||||||
|
val format: String = "m4a",
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PodcastResponse(
|
||||||
|
val url: String = "",
|
||||||
|
val title: String = "",
|
||||||
|
val tracks: Int = 0,
|
||||||
|
val files: List<String> = emptyList(),
|
||||||
|
@SerialName("abs_scan_triggered")
|
||||||
|
val absScanTriggered: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class YandexRequest(
|
||||||
|
val bookid: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class YandexResponse(
|
||||||
|
val bookid: String,
|
||||||
|
val files: List<String> = emptyList(),
|
||||||
|
@SerialName("abs_scan_triggered")
|
||||||
|
val absScanTriggered: Boolean = false,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package org.dueattendant149.bookshelf.data.remote.bookshelfapi.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class LibraryResponse(
|
||||||
|
val id: String,
|
||||||
|
val name: String = "",
|
||||||
|
@SerialName("media_type")
|
||||||
|
val mediaType: String = "",
|
||||||
|
@SerialName("item_count")
|
||||||
|
val itemCount: Int = 0,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
package org.dueattendant149.bookshelf.data.remote.bookshelfapi.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SearchResponse(
|
||||||
|
val results: List<SearchResultItemResponse> = emptyList(),
|
||||||
|
@SerialName("search_time_ms")
|
||||||
|
val searchTimeMs: Int = 0,
|
||||||
|
val total: Int = 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SearchResultItemResponse(
|
||||||
|
val source: String = "",
|
||||||
|
val title: String = "",
|
||||||
|
val author: String = "",
|
||||||
|
val format: String = "",
|
||||||
|
@SerialName("media_type")
|
||||||
|
val mediaType: String = "",
|
||||||
|
@SerialName("size_human")
|
||||||
|
val sizeHuman: String = "",
|
||||||
|
val seeders: Int? = null,
|
||||||
|
val score: Double = 0.0,
|
||||||
|
val guid: String = "",
|
||||||
|
val md5: String = "",
|
||||||
|
@SerialName("magnet_url")
|
||||||
|
val magnetUrl: String = "",
|
||||||
|
@SerialName("download_url")
|
||||||
|
val downloadUrl: String = "",
|
||||||
|
val url: String = "",
|
||||||
|
@SerialName("cover_url")
|
||||||
|
val coverUrl: String = "",
|
||||||
|
@SerialName("info_hash")
|
||||||
|
val infoHash: String = "",
|
||||||
|
@SerialName("download_protocol")
|
||||||
|
val downloadProtocol: String = "",
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SearchRequest(
|
||||||
|
val query: String,
|
||||||
|
val author: String? = null,
|
||||||
|
@SerialName("media_type")
|
||||||
|
val mediaType: String? = null,
|
||||||
|
val format: String? = null,
|
||||||
|
val language: String? = null,
|
||||||
|
@SerialName("year_from")
|
||||||
|
val yearFrom: Int? = null,
|
||||||
|
@SerialName("year_to")
|
||||||
|
val yearTo: Int? = null,
|
||||||
|
val limit: Int = 50,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
package org.dueattendant149.bookshelf.data.remote.bookshelfapi.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class TtsCreateRequest(
|
||||||
|
@SerialName("book_id")
|
||||||
|
val bookId: String,
|
||||||
|
val engine: String = "silero",
|
||||||
|
@SerialName("voice_id")
|
||||||
|
val voiceId: String = "",
|
||||||
|
val speed: Double = 1.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class TtsJobResponse(
|
||||||
|
@SerialName("job_id")
|
||||||
|
val jobId: String,
|
||||||
|
@SerialName("book_id")
|
||||||
|
val bookId: String,
|
||||||
|
val title: String = "",
|
||||||
|
val author: String = "",
|
||||||
|
val engine: String = "",
|
||||||
|
@SerialName("voice_id")
|
||||||
|
val voiceId: String = "",
|
||||||
|
val speed: Double = 1.0,
|
||||||
|
val status: String = "",
|
||||||
|
val progress: Double = 0.0,
|
||||||
|
@SerialName("current_chapter")
|
||||||
|
val currentChapter: String = "",
|
||||||
|
@SerialName("total_chapters")
|
||||||
|
val totalChapters: Int = 0,
|
||||||
|
@SerialName("completed_chapters")
|
||||||
|
val completedChapters: Int = 0,
|
||||||
|
@SerialName("output_path")
|
||||||
|
val outputPath: String = "",
|
||||||
|
val error: String = "",
|
||||||
|
@SerialName("created_at")
|
||||||
|
val createdAt: Double = 0.0,
|
||||||
|
@SerialName("started_at")
|
||||||
|
val startedAt: Double = 0.0,
|
||||||
|
@SerialName("completed_at")
|
||||||
|
val completedAt: Double = 0.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class TtsJobsResponse(
|
||||||
|
val jobs: List<TtsJobResponse> = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class TtsEnginesResponse(
|
||||||
|
val engines: List<TtsEngineResponse> = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class TtsVoicesResponse(
|
||||||
|
val voices: List<TtsVoiceResponse> = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class TtsEngineResponse(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val capabilities: TtsEngineCapabilitiesResponse,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class TtsEngineCapabilitiesResponse(
|
||||||
|
@SerialName("supports_streaming")
|
||||||
|
val supportsStreaming: Boolean = false,
|
||||||
|
@SerialName("supports_cloning")
|
||||||
|
val supportsCloning: Boolean = false,
|
||||||
|
@SerialName("max_text_length")
|
||||||
|
val maxTextLength: Int = 5000,
|
||||||
|
@SerialName("needs_network")
|
||||||
|
val needsNetwork: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class TtsVoiceResponse(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val language: String,
|
||||||
|
val engine: String,
|
||||||
|
val gender: String = "",
|
||||||
|
val quality: String = "",
|
||||||
|
@SerialName("requires_reference")
|
||||||
|
val requiresReference: Boolean = false,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package org.dueattendant149.bookshelf.domain.util
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures the URL ends with a trailing slash so Retrofit can append relative paths.
|
||||||
|
* Also adds a default scheme if missing.
|
||||||
|
*/
|
||||||
|
fun String.fixUriScheme(): String {
|
||||||
|
var url = this.trim()
|
||||||
|
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||||
|
url = "https://$url"
|
||||||
|
}
|
||||||
|
if (!url.endsWith("/")) {
|
||||||
|
url = "$url/"
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
0
gradlew
vendored
Normal file → Executable file
0
gradlew
vendored
Normal file → Executable file
Loading…
Add table
Add a link
Reference in a new issue