Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
7f55ee64fb
Merge eb30f98056 into b67a4a22b5 2026-07-22 16:30:52 -06:00
100 changed files with 2983 additions and 4078 deletions

13
.github/apache_header.txt vendored Normal file
View File

@ -0,0 +1,13 @@
Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

16
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,16 @@
version: 2
enable-beta-ecosystems: true
updates:
- package-ecosystem: "pub"
directory: "/"
schedule:
interval: "daily"
target-branch: "dev"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
target-branch: "dev"

View File

@ -17,23 +17,11 @@
package dev.krtirtho.spotube package dev.krtirtho.spotube
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build import android.os.Build
import org.koin.core.context.GlobalContext
class AndroidPlatform : Platform { class AndroidPlatform : Platform {
override val name: String = "Android ${Build.VERSION.SDK_INT}" override val name: String = "Android ${Build.VERSION.SDK_INT}"
override val type: PlatformType = PlatformType.Android override val type: PlatformType = PlatformType.Android
} }
actual fun getPlatform(): Platform = AndroidPlatform() actual fun getPlatform(): Platform = AndroidPlatform()
actual fun openUrlInBrowser(url: String) {
val context = GlobalContext.get().get<Context>()
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(intent)
}

View File

@ -1,36 +1,24 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by * it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details. * GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.webview package dev.krtirtho.spotube.core.webview
import android.webkit.CookieManager import io.github.kdroidfilter.webview.web.WebViewState
import android.webkit.WebStorage
import android.webkit.WebView actual fun platformWebviewConfig(webView: WebViewState) {
import io.github.kdroidfilter.webview.web.WebViewState webView.webView?.nativeWebView?.settings?.domStorageEnabled = true
actual fun platformWebviewConfig(webView: WebViewState, pluginId: String?) {
val nativeWebView = webView.webView?.nativeWebView as? WebView ?: return
nativeWebView.settings.domStorageEnabled = true
}
actual suspend fun platformClearWebviewData(pluginId: String?) {
if (pluginId == null) return
CookieManager.getInstance().removeAllCookies(null)
CookieManager.getInstance().flush()
WebStorage.getInstance().deleteAllData()
} }

View File

@ -151,7 +151,6 @@
<string name="plugin_action_logout">Logout</string> <string name="plugin_action_logout">Logout</string>
<string name="plugin_section_url_title">Download from URL</string> <string name="plugin_section_url_title">Download from URL</string>
<string name="plugin_section_file_title">Install from file</string> <string name="plugin_section_file_title">Install from file</string>
<string name="plugin_section_install">Install</string>
<string name="plugin_permissions_author_version">• %1$s</string> <string name="plugin_permissions_author_version">• %1$s</string>
<string name="plugin_permissions_requested_title">Permissions requested</string> <string name="plugin_permissions_requested_title">Permissions requested</string>

View File

@ -28,8 +28,6 @@ interface Platform {
expect fun getPlatform(): Platform expect fun getPlatform(): Platform
expect fun openUrlInBrowser(url: String)
fun Platform.isDesktop(): Boolean { fun Platform.isDesktop(): Boolean {
return type == PlatformType.Windows || return type == PlatformType.Windows ||
type == PlatformType.Linux || type == PlatformType.Linux ||

View File

@ -53,7 +53,6 @@ import dev.krtirtho.spotube.modules.library.playlist.LibraryPlaylistsViewModel
import dev.krtirtho.spotube.modules.lyrics.LyricsViewModel import dev.krtirtho.spotube.modules.lyrics.LyricsViewModel
import dev.krtirtho.spotube.modules.playlist.PlaylistRepository import dev.krtirtho.spotube.modules.playlist.PlaylistRepository
import dev.krtirtho.spotube.modules.playlist.PlaylistViewModel import dev.krtirtho.spotube.modules.playlist.PlaylistViewModel
import dev.krtirtho.spotube.modules.plugin.PluginDiscoverViewModel
import dev.krtirtho.spotube.modules.plugin.PluginManager import dev.krtirtho.spotube.modules.plugin.PluginManager
import dev.krtirtho.spotube.modules.plugin.PluginProvider import dev.krtirtho.spotube.modules.plugin.PluginProvider
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksRepository import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksRepository
@ -109,7 +108,6 @@ val sharedModules = module {
// Plugin system // Plugin system
singleOf(::PluginManager) { bind<PluginProvider>() } singleOf(::PluginManager) { bind<PluginProvider>() }
viewModelOf(::PluginDiscoverViewModel)
// Settings // Settings
singleOf(::SettingsRepository) singleOf(::SettingsRepository)

View File

@ -1,22 +0,0 @@
/*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package dev.krtirtho.spotube.core.extras
fun String.kebabToTitleCase(): String {
return this.split("-").joinToString(" ") { it.replaceFirstChar { char -> char.uppercase() } }
}

View File

@ -1,164 +1,157 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by * it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details. * GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.webview package dev.krtirtho.spotube.core.webview
import io.github.kdroidfilter.webview.web.WebContent import io.github.kdroidfilter.webview.web.WebContent
import io.github.kdroidfilter.webview.web.WebViewNavigator import io.github.kdroidfilter.webview.web.WebViewNavigator
import io.github.kdroidfilter.webview.cookie.CookieManager import io.github.kdroidfilter.webview.cookie.CookieManager
import dev.krtirtho.plugin_interfaces.host_apis.Cookie import dev.krtirtho.plugin_interfaces.host_apis.Cookie
import dev.krtirtho.spotube.core.di.injectLogger import dev.krtirtho.spotube.core.di.injectLogger
import dev.krtirtho.spotube.core.navigation.NavigationCommands import dev.krtirtho.spotube.core.navigation.NavigationCommands
import dev.krtirtho.spotube.core.navigation.Routes import dev.krtirtho.spotube.core.navigation.Routes
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
class WebViewController(val navigationCommands: NavigationCommands): KoinComponent { class WebViewController(val navigationCommands: NavigationCommands): KoinComponent {
private val logger by injectLogger<WebViewController>() private val logger by injectLogger<WebViewController>()
private var cookieManager: CookieManager? = null private var cookieManager: CookieManager? = null
private val urlFlow = MutableStateFlow("") private val urlFlow = MutableStateFlow("")
private val webViewCreated = MutableSharedFlow<Unit>(replay = 1) private val webViewCreated = MutableSharedFlow<Unit>(replay = 1)
var currentPluginId: String? = null
private set suspend fun getCookies(url: String): List<Cookie> {
if (cookieManager == null) {
suspend fun getCookies(url: String): List<Cookie> { logger.w { "CookieManager is not initialized. Returning empty cookie list." }
if (cookieManager == null) { return emptyList()
logger.w { "CookieManager is not initialized. Returning empty cookie list." } }
return emptyList() val cookies = cookieManager!!.getCookies(url)
} val cookieList = mutableListOf<Cookie>()
val cookies = cookieManager!!.getCookies(url) cookies.forEach {
val cookieList = mutableListOf<Cookie>() cookieList.add(
cookies.forEach { Cookie(
cookieList.add( name = it.name,
Cookie( value = it.value,
name = it.name, domain = it.domain ?: "",
value = it.value, path = it.path,
domain = it.domain ?: "", expiresAt = it.expiresDate,
path = it.path, secure = it.isSecure ?: false,
expiresAt = it.expiresDate, httpOnly = it.isHttpOnly ?: false
secure = it.isSecure ?: false, )
httpOnly = it.isHttpOnly ?: false )
) }
) return cookieList
} }
return cookieList
} private var content: String? = null
private var isHtmlContent: Boolean = false
private var content: String? = null fun getContent(): String? = content
private var isHtmlContent: Boolean = false fun getWebContent(additionalHttpHeaders: Map<String, String> = emptyMap()): WebContent {
fun getContent(): String? = content if (content == null) throw IllegalStateException("Content is null. This should not happen as WebView should only be opened when content is set.")
fun getWebContent(additionalHttpHeaders: Map<String, String> = emptyMap()): WebContent { if (isHtmlContent) return WebContent.Data(data = content!!, mimeType = "text/html")
if (content == null) throw IllegalStateException("Content is null. This should not happen as WebView should only be opened when content is set.") return WebContent.Url(url = content!!, additionalHttpHeaders = additionalHttpHeaders)
if (isHtmlContent) return WebContent.Data(data = content!!, mimeType = "text/html") }
return WebContent.Url(url = content!!, additionalHttpHeaders = additionalHttpHeaders)
} var webViewNavigator: WebViewNavigator? = null
var webViewNavigator: WebViewNavigator? = null fun emitUrlChange(url: String) {
urlFlow.value = url
fun emitUrlChange(url: String) { }
urlFlow.value = url
} fun emitWebViewCreated() {
webViewCreated.tryEmit(Unit)
fun emitWebViewCreated() { }
webViewCreated.tryEmit(Unit)
} fun setCookieManager(cookieManager: CookieManager) {
this.cookieManager = cookieManager
fun setCookieManager(cookieManager: CookieManager) { }
this.cookieManager = cookieManager
} @OptIn(ExperimentalCoroutinesApi::class)
fun closeWebview() {
@OptIn(ExperimentalCoroutinesApi::class) cookieManager = null
fun closeWebview() { content = null
cookieManager = null isHtmlContent = false
content = null webViewNavigator = null
isHtmlContent = false navigationCommands.pop(Routes.WebView)
webViewNavigator = null urlFlow.value = ""
navigationCommands.pop(Routes.WebView) webViewCreated.resetReplayCache()
urlFlow.value = "" _postMessagesFlow.resetReplayCache()
webViewCreated.resetReplayCache() }
_postMessagesFlow.resetReplayCache()
} fun dispose() {
cookieManager = null
fun dispose() { content = null
cookieManager = null isHtmlContent = false
content = null webViewNavigator = null
isHtmlContent = false }
webViewNavigator = null
} fun navigateTo(url: String) {
if (this.content != null) {
fun navigateTo(url: String, pluginId: String) { throw IllegalStateException("WebView is already open. Please close the current WebView before navigating to a new URL.")
if (this.content != null) { }
throw IllegalStateException("WebView is already open. Please close the current WebView before navigating to a new URL.") this.content = url
} this.isHtmlContent = false
this.currentPluginId = pluginId navigationCommands.navigateTo(Routes.WebView)
this.content = url }
this.isHtmlContent = false
navigationCommands.navigateTo(Routes.WebView) fun navigateToHTML(html: String) {
} if (this.content != null) {
throw IllegalStateException("WebView is already open. Please close the current WebView before navigating to a new URL.")
fun navigateToHTML(html: String, pluginId: String) { }
if (this.content != null) { this.content = html
throw IllegalStateException("WebView is already open. Please close the current WebView before navigating to a new URL.") this.isHtmlContent = true
} navigationCommands.navigateTo(Routes.WebView)
this.currentPluginId = pluginId }
this.content = html
this.isHtmlContent = true suspend fun evaluateJavascript(jsCode: String): String? {
navigationCommands.navigateTo(Routes.WebView) if (webViewNavigator == null) {
} throw IllegalStateException("WebView is not initialized. Cannot evaluate JavaScript.")
}
suspend fun evaluateJavascript(jsCode: String): String? { val completer = CompletableDeferred<String?>()
if (webViewNavigator == null) { try {
throw IllegalStateException("WebView is not initialized. Cannot evaluate JavaScript.") webViewNavigator?.evaluateJavaScript(jsCode) { result ->
} completer.complete(result)
val completer = CompletableDeferred<String?>() }
try { } catch (e: Exception) {
webViewNavigator?.evaluateJavaScript(jsCode) { result -> completer.completeExceptionally(e)
completer.complete(result) throw e
} }
} catch (e: Exception) { return completer.await()
completer.completeExceptionally(e) }
throw e
} suspend fun clearData() {
return completer.await() cookieManager?.removeAllCookies()
} cookieManager = null
content = null
suspend fun clearData(pluginId: String? = null) { isHtmlContent = false
cookieManager?.removeAllCookies() webViewNavigator = null
val targetPluginId = pluginId ?: currentPluginId }
platformClearWebviewData(targetPluginId)
cookieManager = null val urlChangedFlow = urlFlow.asStateFlow()
content = null val webviewCreatedFlow = webViewCreated.asSharedFlow()
isHtmlContent = false private val _postMessagesFlow = MutableSharedFlow<String>(replay = 1)
webViewNavigator = null fun emitPostMessage(message: String) {
currentPluginId = null _postMessagesFlow.tryEmit(message)
} }
val urlChangedFlow = urlFlow.asStateFlow() val postMessagesFlow = _postMessagesFlow.asSharedFlow()
val webviewCreatedFlow = webViewCreated.asSharedFlow()
private val _postMessagesFlow = MutableSharedFlow<String>(replay = 1)
fun emitPostMessage(message: String) {
_postMessagesFlow.tryEmit(message)
}
val postMessagesFlow = _postMessagesFlow.asSharedFlow()
} }

View File

@ -1,24 +1,22 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by * it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details. * GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.webview package dev.krtirtho.spotube.core.webview
import io.github.kdroidfilter.webview.web.WebViewState import io.github.kdroidfilter.webview.web.WebViewState
expect fun platformWebviewConfig(webView: WebViewState, pluginId: String?) expect fun platformWebviewConfig(webView: WebViewState)
expect suspend fun platformClearWebviewData(pluginId: String?)

View File

@ -1,222 +1,228 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by * it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details. * GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.webview package dev.krtirtho.spotube.core.webview
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.IconButton import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.material3.Text
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.text.style.TextAlign import androidx.compose.runtime.setValue
import androidx.compose.ui.unit.dp import androidx.compose.runtime.snapshotFlow
import compose.icons.FeatherIcons import androidx.compose.ui.Alignment
import compose.icons.feathericons.ChevronLeft import androidx.compose.ui.Modifier
import compose.icons.feathericons.ChevronRight import androidx.compose.ui.graphics.Color
import compose.icons.feathericons.X import androidx.compose.ui.text.style.TextAlign
import dev.krtirtho.spotube.core.tools.user_agents.UserAgents import androidx.compose.ui.unit.dp
import io.github.kdroidfilter.webview.jsbridge.IJsMessageHandler import io.github.kdroidfilter.webview.jsbridge.IJsMessageHandler
import io.github.kdroidfilter.webview.jsbridge.JsMessage import io.github.kdroidfilter.webview.jsbridge.JsMessage
import io.github.kdroidfilter.webview.jsbridge.rememberWebViewJsBridge import io.github.kdroidfilter.webview.jsbridge.rememberWebViewJsBridge
import io.github.kdroidfilter.webview.web.WebView import io.github.kdroidfilter.webview.web.WebView
import io.github.kdroidfilter.webview.web.WebViewNavigator import io.github.kdroidfilter.webview.web.rememberWebViewNavigator
import io.github.kdroidfilter.webview.web.WebViewState import io.github.kdroidfilter.webview.web.WebViewState
import io.github.kdroidfilter.webview.web.rememberWebViewNavigator import io.github.kdroidfilter.webview.web.WebViewNavigator
import compose.icons.FeatherIcons
class PostMessageHandler( import compose.icons.feathericons.ChevronLeft
private val onMessageReceived: (String) -> Unit = {} import compose.icons.feathericons.ChevronRight
) : IJsMessageHandler { import compose.icons.feathericons.X
override fun methodName(): String { import dev.krtirtho.spotube.core.tools.user_agents.UserAgents
return "sendMessage" import kotlinx.coroutines.launch
}
class PostMessageHandler(
override fun handle( private val onMessageReceived: (String) -> Unit = {}
message: JsMessage, navigator: WebViewNavigator?, callback: (String) -> Unit ) : IJsMessageHandler {
) { override fun methodName(): String {
onMessageReceived(message.params) return "sendMessage"
callback(message.params) }
}
} override fun handle(
message: JsMessage, navigator: WebViewNavigator?, callback: (String) -> Unit
@Composable ) {
fun PlatformWebViewScreen(webViewController: WebViewController) { onMessageReceived(message.params)
if (webViewController.getContent() == null) { callback(message.params)
// This should never happen, but just in case }
Text("No URL to load") }
return
} @Composable
fun PlatformWebViewScreen(webViewController: WebViewController) {
val state = remember { if (webViewController.getContent() == null) {
WebViewState( // This should never happen, but just in case
webViewController.getWebContent( Text("No URL to load")
additionalHttpHeaders = mapOf( return
"User-Agent" to UserAgents.random() }
)
) val state = remember {
) WebViewState(
}.apply { webViewController.getWebContent(
this.content = webViewController.getWebContent() additionalHttpHeaders = mapOf(
platformWebviewConfig(this, webViewController.currentPluginId) "User-Agent" to UserAgents.random()
} )
)
val navigator = rememberWebViewNavigator() )
val webViewBridge = rememberWebViewJsBridge(navigator) }.apply {
this.content = webViewController.getWebContent()
val bridgeBootstrapScript = remember { platformWebviewConfig(this)
""" }
(function() {
if (typeof window.sendMessage !== "function") { val navigator = rememberWebViewNavigator()
window.sendMessage = function(message) { val webViewBridge = rememberWebViewJsBridge(navigator)
if (typeof message !== "string") {
throw new TypeError("[window.sendMessage] Message must be a string"); val bridgeBootstrapScript = remember {
} """
window.kmpJsBridge.callNative("sendMessage", message); (function() {
}; if (typeof window.sendMessage !== "function") {
} window.sendMessage = function(message) {
if (typeof message !== "string") {
if (!window.bridgeReady) { throw new TypeError("[window.sendMessage] Message must be a string");
const event = new CustomEvent("onBridgeReady"); }
window.dispatchEvent(event); window.kmpJsBridge.callNative("sendMessage", message);
window.bridgeReady = true; };
} }
})();
""".trimIndent() if (!window.bridgeReady) {
} const event = new CustomEvent("onBridgeReady");
window.dispatchEvent(event);
LaunchedEffect(state) { window.bridgeReady = true;
snapshotFlow { state.lastLoadedUrl }.collect { url -> }
if (url != null) { })();
webViewController.emitUrlChange(url) """.trimIndent()
navigator.evaluateJavaScript(bridgeBootstrapScript) }
webViewController.emitWebViewCreated()
} LaunchedEffect(state) {
} snapshotFlow { state.lastLoadedUrl }.collect { url ->
} if (url != null) {
webViewController.emitUrlChange(url)
LaunchedEffect(state.cookieManager, navigator) { navigator.evaluateJavaScript(bridgeBootstrapScript)
webViewController.setCookieManager(cookieManager = state.cookieManager) webViewController.emitWebViewCreated()
webViewController.webViewNavigator = navigator }
} }
}
LaunchedEffect(webViewBridge) {
webViewBridge.register(PostMessageHandler { message -> LaunchedEffect(state.cookieManager, navigator) {
webViewController.emitPostMessage(message) webViewController.setCookieManager(cookieManager = state.cookieManager)
}) webViewController.webViewNavigator = navigator
} }
DisposableEffect(Unit) { LaunchedEffect(webViewBridge) {
onDispose { webViewBridge.register(PostMessageHandler { message ->
webViewController.dispose() webViewController.emitPostMessage(message)
} })
} }
Scaffold( DisposableEffect(Unit) {
contentWindowInsets = WindowInsets.statusBars, onDispose {
topBar = { webViewController.dispose()
Row( }
modifier = Modifier.fillMaxWidth().statusBarsPadding().height(56.dp), }
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically Scaffold(
) { contentWindowInsets = WindowInsets.statusBars,
Row( topBar = {
verticalAlignment = Alignment.CenterVertically, Row(
horizontalArrangement = Arrangement.Start, modifier = Modifier.fillMaxWidth().statusBarsPadding().height(56.dp),
modifier = Modifier.height(56.dp) horizontalArrangement = Arrangement.SpaceBetween,
) { verticalAlignment = Alignment.CenterVertically
IconButton( ) {
onClick = { Row(
navigator.navigateBack() verticalAlignment = Alignment.CenterVertically,
}, enabled = navigator.canGoBack horizontalArrangement = Arrangement.Start,
) { modifier = Modifier.height(56.dp)
Icon( ) {
FeatherIcons.ChevronLeft, IconButton(
contentDescription = "Go back to browser history" onClick = {
) navigator.navigateBack()
} }, enabled = navigator.canGoBack
IconButton( ) {
onClick = { Icon(
navigator.navigateForward() FeatherIcons.ChevronLeft,
}, enabled = navigator.canGoForward contentDescription = "Go back to browser history"
) { )
Icon( }
FeatherIcons.ChevronRight, IconButton(
contentDescription = "Go forward to browser history" onClick = {
) navigator.navigateForward()
} }, enabled = navigator.canGoForward
} ) {
Surface( Icon(
modifier = Modifier.weight(1f).height(36.dp).padding(horizontal = 4.dp), FeatherIcons.ChevronRight,
shape = RoundedCornerShape(18.dp), contentDescription = "Go forward to browser history"
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), )
border = BorderStroke(1.dp, Color.Gray.copy(alpha = 0.5f)) }
) { }
BasicTextField( Surface(
value = state.lastLoadedUrl ?: "", modifier = Modifier.weight(1f).height(36.dp).padding(horizontal = 4.dp),
onValueChange = {}, // Read-only shape = RoundedCornerShape(18.dp),
readOnly = true, color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
singleLine = true, border = BorderStroke(1.dp, Color.Gray.copy(alpha = 0.5f))
textStyle = MaterialTheme.typography.bodyMedium.copy( ) {
color = MaterialTheme.colorScheme.onSurface, BasicTextField(
textAlign = TextAlign.Start value = state.lastLoadedUrl ?: "",
), onValueChange = {}, // Read-only
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp) readOnly = true,
.wrapContentHeight(Alignment.CenterVertically) singleLine = true,
) textStyle = MaterialTheme.typography.bodyMedium.copy(
} color = MaterialTheme.colorScheme.onSurface,
IconButton( textAlign = TextAlign.Start
onClick = { ),
webViewController.closeWebview() modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp)
}) { .wrapContentHeight(Alignment.CenterVertically)
Icon(FeatherIcons.X, contentDescription = "Close WebView") )
} }
} IconButton(
}) { innerPadding -> onClick = {
WebView( webViewController.closeWebview()
state = state, }) {
modifier = Modifier.padding(innerPadding).fillMaxSize(), Icon(FeatherIcons.X, contentDescription = "Close WebView")
navigator = navigator, }
webViewJsBridge = webViewBridge, }
onCreated = { webView -> }) { innerPadding ->
navigator.evaluateJavaScript(bridgeBootstrapScript) WebView(
state = state,
}, modifier = Modifier.padding(innerPadding).fillMaxSize(),
factory = null navigator = navigator,
) webViewJsBridge = webViewBridge,
} onCreated = { webView ->
navigator.evaluateJavaScript(bridgeBootstrapScript)
},
factory = null
)
}
} }

View File

@ -1,345 +1,345 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by * it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details. * GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.zipline package dev.krtirtho.spotube.core.zipline
import app.cash.zipline.Zipline import app.cash.zipline.Zipline
import app.cash.zipline.ZiplineService import app.cash.zipline.ZiplineService
import app.cash.zipline.loader.DefaultFreshnessCheckerNotFresh import app.cash.zipline.loader.DefaultFreshnessCheckerNotFresh
import app.cash.zipline.loader.LoadResult import app.cash.zipline.loader.LoadResult
import app.cash.zipline.loader.ManifestVerifier import app.cash.zipline.loader.ManifestVerifier
import app.cash.zipline.loader.ZiplineLoader import app.cash.zipline.loader.ZiplineLoader
import dev.krtirtho.plugin_interfaces.core.Initializer import dev.krtirtho.plugin_interfaces.core.Initializer
import dev.krtirtho.plugin_interfaces.core.Initializer_SERVICE_NAME import dev.krtirtho.plugin_interfaces.core.Initializer_SERVICE_NAME
import dev.krtirtho.plugin_interfaces.host_apis.CryptoAPI import dev.krtirtho.plugin_interfaces.host_apis.CryptoAPI
import dev.krtirtho.plugin_interfaces.host_apis.CryptoAPI_SERVICE_NAME import dev.krtirtho.plugin_interfaces.host_apis.CryptoAPI_SERVICE_NAME
import dev.krtirtho.plugin_interfaces.host_apis.HttpClientAPI import dev.krtirtho.plugin_interfaces.host_apis.HttpClientAPI
import dev.krtirtho.plugin_interfaces.host_apis.HttpClientAPI_SERVICE_NAME import dev.krtirtho.plugin_interfaces.host_apis.HttpClientAPI_SERVICE_NAME
import dev.krtirtho.plugin_interfaces.host_apis.PersistedStorageAPI import dev.krtirtho.plugin_interfaces.host_apis.PersistedStorageAPI
import dev.krtirtho.plugin_interfaces.host_apis.PersistedStorageAPI_SERVICE_NAME import dev.krtirtho.plugin_interfaces.host_apis.PersistedStorageAPI_SERVICE_NAME
import dev.krtirtho.plugin_interfaces.host_apis.SystemInformationAPI import dev.krtirtho.plugin_interfaces.host_apis.SystemInformationAPI
import dev.krtirtho.plugin_interfaces.host_apis.SystemInformationAPI_SERVICE_NAME import dev.krtirtho.plugin_interfaces.host_apis.SystemInformationAPI_SERVICE_NAME
import dev.krtirtho.plugin_interfaces.host_apis.WebViewAPI import dev.krtirtho.plugin_interfaces.host_apis.WebViewAPI
import dev.krtirtho.plugin_interfaces.host_apis.WebViewAPI_SERVICE_NAME import dev.krtirtho.plugin_interfaces.host_apis.WebViewAPI_SERVICE_NAME
import dev.krtirtho.plugin_interfaces.plugin_apis.audio.AudioAPI import dev.krtirtho.plugin_interfaces.plugin_apis.audio.AudioAPI
import dev.krtirtho.plugin_interfaces.plugin_apis.audio.AudioAPI_SERVICE_NAME import dev.krtirtho.plugin_interfaces.plugin_apis.audio.AudioAPI_SERVICE_NAME
import dev.krtirtho.plugin_interfaces.plugin_apis.core.CoreAPI import dev.krtirtho.plugin_interfaces.plugin_apis.core.CoreAPI
import dev.krtirtho.plugin_interfaces.plugin_apis.core.CoreAPI_SERVICE_NAME import dev.krtirtho.plugin_interfaces.plugin_apis.core.CoreAPI_SERVICE_NAME
import dev.krtirtho.plugin_interfaces.plugin_apis.lyrics.LyricsAPI import dev.krtirtho.plugin_interfaces.plugin_apis.lyrics.LyricsAPI
import dev.krtirtho.plugin_interfaces.plugin_apis.lyrics.LyricsAPI_SERVICE_NAME import dev.krtirtho.plugin_interfaces.plugin_apis.lyrics.LyricsAPI_SERVICE_NAME
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.album.MetadataAlbumAPI import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.album.MetadataAlbumAPI
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.album.MetadataAlbumAPI_SERVICE_NAME import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.album.MetadataAlbumAPI_SERVICE_NAME
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.artist.MetadataArtistAPI import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.artist.MetadataArtistAPI
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.artist.MetadataArtistAPI_SERVICE_NAME import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.artist.MetadataArtistAPI_SERVICE_NAME
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.browse.MetadataBrowseAPI import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.browse.MetadataBrowseAPI
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.browse.MetadataBrowseAPI_SERVICE_NAME import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.browse.MetadataBrowseAPI_SERVICE_NAME
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.playlist.MetadataPlaylistAPI import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.playlist.MetadataPlaylistAPI
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.playlist.MetadataPlaylistAPI_SERVICE_NAME import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.playlist.MetadataPlaylistAPI_SERVICE_NAME
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.search.MetadataSearchAPI import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.search.MetadataSearchAPI
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.search.MetadataSearchAPI_SERVICE_NAME import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.search.MetadataSearchAPI_SERVICE_NAME
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrackAPI import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrackAPI
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrackAPI_SERVICE_NAME import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrackAPI_SERVICE_NAME
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.user.MetadataUserAPI import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.user.MetadataUserAPI
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.user.MetadataUserAPI_SERVICE_NAME import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.user.MetadataUserAPI_SERVICE_NAME
import dev.krtirtho.plugin_interfaces.plugin_apis.scrobble.ScrobbleAPI import dev.krtirtho.plugin_interfaces.plugin_apis.scrobble.ScrobbleAPI
import dev.krtirtho.plugin_interfaces.plugin_apis.scrobble.ScrobbleAPI_SERVICE_NAME import dev.krtirtho.plugin_interfaces.plugin_apis.scrobble.ScrobbleAPI_SERVICE_NAME
import dev.krtirtho.spotube.core.di.injectLogger import dev.krtirtho.spotube.core.di.injectLogger
import dev.krtirtho.spotube.core.webview.WebViewController import dev.krtirtho.spotube.core.webview.WebViewController
import dev.krtirtho.spotube.core.zipline.host_apis.RealCryptoAPI import dev.krtirtho.spotube.core.zipline.host_apis.RealCryptoAPI
import dev.krtirtho.spotube.core.zipline.host_apis.RealHttpClientAPI import dev.krtirtho.spotube.core.zipline.host_apis.RealHttpClientAPI
import dev.krtirtho.spotube.core.zipline.host_apis.RealPersistedStorageAPI import dev.krtirtho.spotube.core.zipline.host_apis.RealPersistedStorageAPI
import dev.krtirtho.spotube.core.zipline.host_apis.RealSystemInformationAPI import dev.krtirtho.spotube.core.zipline.host_apis.RealSystemInformationAPI
import dev.krtirtho.spotube.core.zipline.host_apis.RealWebViewAPI import dev.krtirtho.spotube.core.zipline.host_apis.RealWebViewAPI
import dev.krtirtho.spotube.modules.plugin.PluginAbility import dev.krtirtho.spotube.modules.plugin.PluginAbility
import dev.krtirtho.spotube.modules.plugin.PluginCapability import dev.krtirtho.spotube.modules.plugin.PluginCapability
import dev.krtirtho.spotube.modules.plugin.PluginEntry import dev.krtirtho.spotube.modules.plugin.PluginEntry
import io.ktor.http.URLBuilder import io.ktor.http.URLBuilder
import io.ktor.http.decodeURLQueryComponent import io.ktor.http.decodeURLQueryComponent
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okio.Path.Companion.toPath import okio.Path.Companion.toPath
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import kotlin.reflect.KClass import kotlin.reflect.KClass
/** /**
* Manages the lifecycle of a Zipline plugin. * Manages the lifecycle of a Zipline plugin.
* It runs everything on its own dispatcher (different thread) as per Zipline's requirements. * It runs everything on its own dispatcher (different thread) as per Zipline's requirements.
* Anything it provides, must be called within that dispatcher context. * Anything it provides, must be called within that dispatcher context.
* The [use] function must be used. * The [use] function must be used.
* *
* The host bindings are called by plugins in the supplied [ZiplineDispatcher] as well, * The host bindings are called by plugins in the supplied [ZiplineDispatcher] as well,
* so if they are calling something on [Dispatchers.Main], those calls should be wrapped in * so if they are calling something on [Dispatchers.Main], those calls should be wrapped in
* `withContext(Dispatchers.Main)` to avoid blocking the zipline thread. It can cause stack-overflows. * `withContext(Dispatchers.Main)` to avoid blocking the zipline thread. It can cause stack-overflows.
* *
* The plugin is loaded lazily when [start] is called, and all services are closed when [stop] is called. * The plugin is loaded lazily when [start] is called, and all services are closed when [stop] is called.
*/ */
open class ZiplinePluginService( open class ZiplinePluginService(
val applicationName: String, val applicationName: String,
private val manifestUrl: String, private val manifestUrl: String,
private val pluginInfo: PluginEntry, private val pluginInfo: PluginEntry,
) : PluginService, KoinComponent { ) : PluginService, KoinComponent {
// QuickJS compile() uses deep C-level recursion on the native thread stack. // QuickJS compile() uses deep C-level recursion on the native thread stack.
// Zipline.create() sets maxStackSize to only 6 MiB, but compiling large JS modules // Zipline.create() sets maxStackSize to only 6 MiB, but compiling large JS modules
// (e.g. kotlin-stdlib at ~491 KB) can exceed that during AST parsing. // (e.g. kotlin-stdlib at ~491 KB) can exceed that during AST parsing.
// We use a custom EventListener to increase maxStackSize right after the Zipline // We use a custom EventListener to increase maxStackSize right after the Zipline
// instance is created, before any modules are loaded. // instance is created, before any modules are loaded.
private val ziplineDispatcher = createZiplineDispatcher() private val ziplineDispatcher = createZiplineDispatcher()
private fun trace(event: String) { private fun trace(event: String) {
logger.d { "[$applicationName] $event" } logger.d { "[$applicationName] $event" }
} }
private val logger by injectLogger<ZiplinePluginService>() private val logger by injectLogger<ZiplinePluginService>()
private val webViewController: WebViewController by inject() private val webViewController: WebViewController by inject()
private val scope = CoroutineScope(SupervisorJob() + ziplineDispatcher.dispatcher) private val scope = CoroutineScope(SupervisorJob() + ziplineDispatcher.dispatcher)
private val ziplineExceptionHandler = CoroutineExceptionHandler { _, throwable -> private val ziplineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
logger.e(throwable) { "Zipline Engine Error" } logger.e(throwable) { "Zipline Engine Error" }
} }
private val lifecycleMutex = Mutex() private val lifecycleMutex = Mutex()
private var ziplineLoader: ZiplineLoader private var ziplineLoader: ZiplineLoader
private var ziplineInstance: Zipline? = null private var ziplineInstance: Zipline? = null
private val serviceRegistry = mutableMapOf<KClass<*>, ZiplineService>() private val serviceRegistry = mutableMapOf<KClass<*>, ZiplineService>()
init { init {
val manifestPath = URLBuilder(manifestUrl) val manifestPath = URLBuilder(manifestUrl)
val baseDir = val baseDir =
manifestPath.encodedParameters["path"]?.decodeURLQueryComponent()?.toPath()?.parent manifestPath.encodedParameters["path"]?.decodeURLQueryComponent()?.toPath()?.parent
?: throw IllegalArgumentException("Invalid manifest URL: $manifestUrl. Expected a 'path' query parameter pointing to the manifest file.") ?: throw IllegalArgumentException("Invalid manifest URL: $manifestUrl. Expected a 'path' query parameter pointing to the manifest file.")
ziplineLoader = ZiplineLoader( ziplineLoader = ZiplineLoader(
dispatcher = ziplineDispatcher.dispatcher, dispatcher = ziplineDispatcher.dispatcher,
manifestVerifier = ManifestVerifier.NO_SIGNATURE_CHECKS, manifestVerifier = ManifestVerifier.NO_SIGNATURE_CHECKS,
httpClient = FileSystemHTTPClient(baseDir) httpClient = FileSystemHTTPClient(baseDir)
) )
} }
private val realHttpClientAPI = RealHttpClientAPI() private val realHttpClientAPI = RealHttpClientAPI()
private val realWebViewAPI = RealWebViewAPI(scope, webViewController, pluginInfo.id) private val realWebViewAPI = RealWebViewAPI(scope, webViewController)
private val persistedStorageAPI = RealPersistedStorageAPI(pluginInfo) private val persistedStorageAPI = RealPersistedStorageAPI(pluginInfo)
private val cryptoAPI = RealCryptoAPI(scope.coroutineContext) private val cryptoAPI = RealCryptoAPI(scope.coroutineContext)
private val systemInformationAPI = RealSystemInformationAPI() private val systemInformationAPI = RealSystemInformationAPI()
private val loggedInStateFlow = MutableStateFlow(false) private val loggedInStateFlow = MutableStateFlow(false)
override val loggedInFlow: StateFlow<Boolean> = loggedInStateFlow.asStateFlow() override val loggedInFlow: StateFlow<Boolean> = loggedInStateFlow.asStateFlow()
private fun bindHostServices(zipline: Zipline) { private fun bindHostServices(zipline: Zipline) {
trace("initializer(): binding host APIs") trace("initializer(): binding host APIs")
logger.d { "[$applicationName] Binding host APIs in initializer" } logger.d { "[$applicationName] Binding host APIs in initializer" }
try { try {
// Basic APIs // Basic APIs
zipline.bind<CryptoAPI>(CryptoAPI_SERVICE_NAME, cryptoAPI) zipline.bind<CryptoAPI>(CryptoAPI_SERVICE_NAME, cryptoAPI)
zipline.bind<SystemInformationAPI>( zipline.bind<SystemInformationAPI>(
SystemInformationAPI_SERVICE_NAME, SystemInformationAPI_SERVICE_NAME,
systemInformationAPI systemInformationAPI
) )
// Conditional APIs based on plugin capabilities // Conditional APIs based on plugin capabilities
if (PluginCapability.NETWORK_REQUESTS in pluginInfo.capabilities) { if (PluginCapability.NETWORK_REQUESTS in pluginInfo.capabilities) {
zipline.bind<HttpClientAPI>( zipline.bind<HttpClientAPI>(
HttpClientAPI_SERVICE_NAME, HttpClientAPI_SERVICE_NAME,
realHttpClientAPI realHttpClientAPI
) )
} }
if (PluginCapability.WEBVIEW in pluginInfo.capabilities) { if (PluginCapability.WEBVIEW in pluginInfo.capabilities) {
zipline.bind<WebViewAPI>(WebViewAPI_SERVICE_NAME, realWebViewAPI) zipline.bind<WebViewAPI>(WebViewAPI_SERVICE_NAME, realWebViewAPI)
} }
if (PluginCapability.PERSISTENT_STORAGE in pluginInfo.capabilities) { if (PluginCapability.PERSISTENT_STORAGE in pluginInfo.capabilities) {
zipline.bind<PersistedStorageAPI>( zipline.bind<PersistedStorageAPI>(
PersistedStorageAPI_SERVICE_NAME, PersistedStorageAPI_SERVICE_NAME,
persistedStorageAPI persistedStorageAPI
) )
} }
} catch (e: Exception) { } catch (e: Exception) {
logger.e(e) { "[$applicationName] Failed to bind host APIs: ${e.message}" } logger.e(e) { "[$applicationName] Failed to bind host APIs: ${e.message}" }
throw e throw e
} }
} }
private fun consumePluginServices(result: LoadResult.Success) { private fun consumePluginServices(result: LoadResult.Success) {
trace("start(): loadOnce success") trace("start(): loadOnce success")
val apiMap = val apiMap =
buildMap<KClass<*>, ZiplineService> { buildMap<KClass<*>, ZiplineService> {
put(CoreAPI::class, result.zipline.take<CoreAPI>(CoreAPI_SERVICE_NAME)) put(CoreAPI::class, result.zipline.take<CoreAPI>(CoreAPI_SERVICE_NAME))
if (PluginAbility.METADATA in pluginInfo.abilities) { if (PluginAbility.METADATA in pluginInfo.abilities) {
put( put(
MetadataUserAPI::class, MetadataUserAPI::class,
result.zipline.take<MetadataUserAPI>( result.zipline.take<MetadataUserAPI>(
MetadataUserAPI_SERVICE_NAME MetadataUserAPI_SERVICE_NAME
) )
) )
put( put(
MetadataTrackAPI::class, MetadataTrackAPI::class,
result.zipline.take<MetadataTrackAPI>( result.zipline.take<MetadataTrackAPI>(
MetadataTrackAPI_SERVICE_NAME MetadataTrackAPI_SERVICE_NAME
) )
) )
put( put(
MetadataAlbumAPI::class, MetadataAlbumAPI::class,
result.zipline.take<MetadataAlbumAPI>( result.zipline.take<MetadataAlbumAPI>(
MetadataAlbumAPI_SERVICE_NAME MetadataAlbumAPI_SERVICE_NAME
) )
) )
put( put(
MetadataArtistAPI::class, MetadataArtistAPI::class,
result.zipline.take<MetadataArtistAPI>( result.zipline.take<MetadataArtistAPI>(
MetadataArtistAPI_SERVICE_NAME MetadataArtistAPI_SERVICE_NAME
) )
) )
put( put(
MetadataPlaylistAPI::class, MetadataPlaylistAPI::class,
result.zipline.take<MetadataPlaylistAPI>( result.zipline.take<MetadataPlaylistAPI>(
MetadataPlaylistAPI_SERVICE_NAME MetadataPlaylistAPI_SERVICE_NAME
) )
) )
put( put(
MetadataBrowseAPI::class, MetadataBrowseAPI::class,
result.zipline.take<MetadataBrowseAPI>( result.zipline.take<MetadataBrowseAPI>(
MetadataBrowseAPI_SERVICE_NAME MetadataBrowseAPI_SERVICE_NAME
) )
) )
put( put(
MetadataSearchAPI::class, MetadataSearchAPI::class,
result.zipline.take<MetadataSearchAPI>( result.zipline.take<MetadataSearchAPI>(
MetadataSearchAPI_SERVICE_NAME MetadataSearchAPI_SERVICE_NAME
) )
) )
} }
if (PluginAbility.AUDIO in pluginInfo.abilities) { if (PluginAbility.AUDIO in pluginInfo.abilities) {
put( put(
AudioAPI::class, AudioAPI::class,
result.zipline.take<AudioAPI>(AudioAPI_SERVICE_NAME) result.zipline.take<AudioAPI>(AudioAPI_SERVICE_NAME)
) )
} }
if (PluginAbility.LYRICS in pluginInfo.abilities) { if (PluginAbility.LYRICS in pluginInfo.abilities) {
put( put(
LyricsAPI::class, LyricsAPI::class,
result.zipline.take<LyricsAPI>(LyricsAPI_SERVICE_NAME) result.zipline.take<LyricsAPI>(LyricsAPI_SERVICE_NAME)
) )
} }
if (PluginAbility.SCROBBLE in pluginInfo.abilities) { if (PluginAbility.SCROBBLE in pluginInfo.abilities) {
put( put(
ScrobbleAPI::class, ScrobbleAPI::class,
result.zipline.take<ScrobbleAPI>(ScrobbleAPI_SERVICE_NAME) result.zipline.take<ScrobbleAPI>(ScrobbleAPI_SERVICE_NAME)
) )
} }
} }
serviceRegistry.putAll(apiMap) serviceRegistry.putAll(apiMap)
trace("start(): API ready") trace("start(): API ready")
} }
private fun runLogInFlowObservers() = scope.launch { private fun runLogInFlowObservers() = scope.launch {
val coreAPI = serviceRegistry[CoreAPI::class] as CoreAPI val coreAPI = serviceRegistry[CoreAPI::class] as CoreAPI
coreAPI.loggedInFlow.collect { isLoggedIn -> coreAPI.loggedInFlow.collect { isLoggedIn ->
loggedInStateFlow.value = isLoggedIn loggedInStateFlow.value = isLoggedIn
} }
} }
override suspend fun start() { override suspend fun start() {
lifecycleMutex.withLock { lifecycleMutex.withLock {
trace("start(): entered") trace("start(): entered")
if (serviceRegistry.isNotEmpty()) { if (serviceRegistry.isNotEmpty()) {
trace("start(): already started, skipping") trace("start(): already started, skipping")
return return
} }
logger.d { "[$applicationName] start(): loading plugin from $manifestUrl" } logger.d { "[$applicationName] start(): loading plugin from $manifestUrl" }
withContext(ziplineDispatcher.dispatcher) { withContext(ziplineDispatcher.dispatcher) {
trace("start(): inside zipline dispatcher before loadOnce") trace("start(): inside zipline dispatcher before loadOnce")
val result = ziplineLoader.loadOnce( val result = ziplineLoader.loadOnce(
applicationName = applicationName, applicationName = applicationName,
manifestUrl = manifestUrl, manifestUrl = manifestUrl,
freshnessChecker = DefaultFreshnessCheckerNotFresh, freshnessChecker = DefaultFreshnessCheckerNotFresh,
) )
when (result) { when (result) {
is LoadResult.Success -> { is LoadResult.Success -> {
logger.d { "[$applicationName] start(): loadOnce succeeded, consuming services" } logger.d { "[$applicationName] start(): loadOnce succeeded, consuming services" }
ziplineInstance = result.zipline ziplineInstance = result.zipline
// Now we consume the initializer // Now we consume the initializer
val initializer = result.zipline.take<Initializer>(Initializer_SERVICE_NAME) val initializer = result.zipline.take<Initializer>(Initializer_SERVICE_NAME)
// Bind host services before initialization, so plugins can use them in their initializer // Bind host services before initialization, so plugins can use them in their initializer
bindHostServices(result.zipline) bindHostServices(result.zipline)
trace("start(): calling initializer.initialize()") trace("start(): calling initializer.initialize()")
runCatching { initializer.initialize() } runCatching { initializer.initialize() }
.onSuccess { .onSuccess {
consumePluginServices(result) consumePluginServices(result)
runLogInFlowObservers() runLogInFlowObservers()
} }
.onFailure { e -> .onFailure { e ->
logger.e(e) { "[$applicationName] Initializer failed: ${e.message}" } logger.e(e) { "[$applicationName] Initializer failed: ${e.message}" }
throw e throw e
} }
} }
is LoadResult.Failure -> { is LoadResult.Failure -> {
trace("start(): loadOnce failure: ${result.exception}") trace("start(): loadOnce failure: ${result.exception}")
logger.e(result.exception) { "[$applicationName] Failed to load plugin: ${result.exception.message}" } logger.e(result.exception) { "[$applicationName] Failed to load plugin: ${result.exception.message}" }
throw result.exception throw result.exception
} }
} }
} }
} }
} }
override suspend fun stop() { override suspend fun stop() {
lifecycleMutex.withLock { lifecycleMutex.withLock {
trace("stop(): entered") trace("stop(): entered")
withContext(ziplineDispatcher.dispatcher) { withContext(ziplineDispatcher.dispatcher) {
ziplineInstance?.close() ziplineInstance?.close()
ziplineInstance = null ziplineInstance = null
for (service in serviceRegistry.values) { for (service in serviceRegistry.values) {
try { try {
trace("stop(): closing service ${service::class.simpleName}") trace("stop(): closing service ${service::class.simpleName}")
service.close() service.close()
} catch (_: Exception) { } catch (_: Exception) {
trace("stop(): error closing service ${service::class.simpleName}") trace("stop(): error closing service ${service::class.simpleName}")
} }
} }
} }
serviceRegistry.clear() serviceRegistry.clear()
scope.cancel() scope.cancel()
loggedInStateFlow.value = false loggedInStateFlow.value = false
ziplineDispatcher.close() ziplineDispatcher.close()
trace("stop(): completed") trace("stop(): completed")
} }
} }
override suspend fun <T> use(block: suspend PluginServiceScope.() -> T): T { override suspend fun <T> use(block: suspend PluginServiceScope.() -> T): T {
return withContext(ziplineDispatcher.dispatcher + ziplineExceptionHandler) { return withContext(ziplineDispatcher.dispatcher + ziplineExceptionHandler) {
// Create the scope with the current registry // Create the scope with the current registry
val scope = PluginServiceScope(serviceRegistry) val scope = PluginServiceScope(serviceRegistry)
// Execute the block with 'scope' as 'this' // Execute the block with 'scope' as 'this'
scope.block() scope.block()
} }
} }
} }

View File

@ -1,74 +1,76 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by * it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details. * GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.zipline.host_apis package dev.krtirtho.spotube.core.zipline.host_apis
import dev.krtirtho.plugin_interfaces.host_apis.Cookie import dev.krtirtho.plugin_interfaces.host_apis.Cookie
import dev.krtirtho.plugin_interfaces.host_apis.WebViewAPI import dev.krtirtho.plugin_interfaces.host_apis.WebViewAPI
import dev.krtirtho.spotube.core.webview.WebViewController import dev.krtirtho.spotube.core.webview.WebViewController
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
class RealWebViewAPI( import kotlinx.coroutines.launch
private val scope: CoroutineScope, import kotlinx.coroutines.withContext
private val webViewController: WebViewController,
private val pluginId: String, class RealWebViewAPI(
) : WebViewAPI { private val scope: CoroutineScope,
override fun navigateTo(url: String) { private val webViewController: WebViewController,
scope.launch(Dispatchers.Main) { ) : WebViewAPI {
webViewController.navigateTo(url, pluginId) override fun navigateTo(url: String) {
} scope.launch(Dispatchers.Main) {
} webViewController.navigateTo(url)
}
override fun navigateToHTML(html: String) { }
scope.launch {
webViewController.navigateToHTML(html, pluginId) override fun navigateToHTML(html: String) {
} scope.launch {
} webViewController.navigateToHTML(html)
}
override suspend fun getCookies(url: String): List<Cookie> { }
return withContext(Dispatchers.Main) {
webViewController.getCookies(url) override suspend fun getCookies(url: String): List<Cookie> {
} return withContext(Dispatchers.Main) {
} webViewController.getCookies(url)
}
override suspend fun evaluateJavaScript(script: String): String? { }
return withContext(Dispatchers.Main) {
webViewController.evaluateJavascript(script) override suspend fun evaluateJavaScript(script: String): String? {
} return withContext(Dispatchers.Main) {
} webViewController.evaluateJavascript(script)
}
override fun urlChangeFlow(): Flow<String> { }
return webViewController.urlChangedFlow
} override fun urlChangeFlow(): Flow<String> {
override fun webviewCreatedFlow(): Flow<Unit> { return webViewController.urlChangedFlow
return webViewController.webviewCreatedFlow }
} override fun webviewCreatedFlow(): Flow<Unit> {
override fun postMessagesFlow(): Flow<String> { return webViewController.webviewCreatedFlow
return webViewController.postMessagesFlow }
} override fun postMessagesFlow(): Flow<String> {
return webViewController.postMessagesFlow
override fun exitWebView() { }
scope.launch(Dispatchers.Main) {
webViewController.closeWebview() override fun exitWebView() {
} scope.launch(Dispatchers.Main) {
} webViewController.closeWebview()
}
}
} }

View File

@ -1,2 +0,0 @@
package dev.krtirtho.spotube.modules.blacklist

View File

@ -27,11 +27,7 @@ val NEWPIPE_YOUTUBE_BUILT_IN_PLUGIN = PluginEntry(
PluginCapability.NETWORK_REQUESTS, PluginCapability.NETWORK_REQUESTS,
PluginCapability.PERSISTENT_STORAGE PluginCapability.PERSISTENT_STORAGE
), ),
abilities = listOf(PluginAbility.AUDIO), abilities = listOf(PluginAbility.AUDIO)
contact = "",
repository = "",
bugs = "",
license = "",
) )
val LRCLIB_BUILT_IN_PLUGIN = PluginEntry( val LRCLIB_BUILT_IN_PLUGIN = PluginEntry(
name = "LRCLib Lyrics", name = "LRCLib Lyrics",
@ -42,11 +38,7 @@ val LRCLIB_BUILT_IN_PLUGIN = PluginEntry(
capabilities = listOf( capabilities = listOf(
PluginCapability.NETWORK_REQUESTS, PluginCapability.NETWORK_REQUESTS,
), ),
abilities = listOf(PluginAbility.LYRICS), abilities = listOf(PluginAbility.LYRICS)
contact = "",
repository = "",
bugs = "",
license = "",
) )
val BUILT_IN_PLUGINS = listOf( val BUILT_IN_PLUGINS = listOf(
NEWPIPE_YOUTUBE_BUILT_IN_PLUGIN, NEWPIPE_YOUTUBE_BUILT_IN_PLUGIN,

View File

@ -1,125 +0,0 @@
/*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package dev.krtirtho.spotube.modules.plugin
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.get
import io.ktor.client.request.headers
import io.ktor.client.request.parameter
import io.ktor.http.append
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@Serializable
data class GitHubRepoSearchResponse(
@SerialName("total_count") val totalCount: Int,
@SerialName("incomplete_results") val incompleteResults: Boolean,
val items: List<GitHubRepo>,
)
@Serializable
data class GitHubRepo(
val id: Long,
@SerialName("full_name") val fullName: String,
val description: String? = null,
@SerialName("stargazers_count") val stargazersCount: Int = 0,
@SerialName("html_url") val htmlUrl: String,
val owner: GitHubOwner,
)
@Serializable
data class GitHubOwner(
val login: String,
@SerialName("avatar_url") val avatarUrl: String,
)
@Serializable
data class GitHubRelease(
@SerialName("tag_name") val tagName: String,
val name: String? = null,
val body: String? = null,
val assets: List<GitHubAsset>,
@SerialName("html_url") val htmlUrl: String,
val prerelease: Boolean = false,
val draft: Boolean = false,
)
@Serializable
data class GitHubAsset(
val name: String,
@SerialName("browser_download_url") val browserDownloadUrl: String,
)
class GitHubPluginRepository {
private val httpClient = HttpClient {
install(ContentNegotiation) {
json(Json { ignoreUnknownKeys = true })
}
}
suspend fun searchSpotubePlugins(page: Int = 1, perPage: Int = 30): GitHubRepoSearchResponse {
return httpClient.get("https://api.github.com/search/repositories") {
headers {
append("Accept", "application/vnd.github+json")
append("X-GitHub-Api-Version", "2022-11-28")
}
parameter("q", "topic:spotube-zipline-plugin")
parameter("sort", "stars")
parameter("order", "desc")
parameter("page", page)
parameter("per_page", perPage)
}.body()
}
suspend fun getLatestReleaseSmplugUrl(owner: String, repo: String): String? {
return try {
val release: GitHubRelease =
httpClient.get("https://api.github.com/repos/$owner/$repo/releases/latest") {
headers {
append("Accept", "application/vnd.github+json")
append("X-GitHub-Api-Version", "2022-11-28")
}
}.body()
release.assets.firstOrNull { it.name.endsWith(".smplug") }?.browserDownloadUrl
} catch (_: Exception) {
null
}
}
suspend fun getReleases(owner: String, repo: String, perPage: Int = 30): List<GitHubRelease> {
return try {
httpClient.get("https://api.github.com/repos/$owner/$repo/releases") {
headers {
append("Accept", "application/vnd.github+json")
append("X-GitHub-Api-Version", "2022-11-28")
}
parameter("per_page", perPage)
}.body()
} catch (_: Exception) {
emptyList()
}
}
fun close() {
httpClient.close()
}
}

View File

@ -1,180 +0,0 @@
/*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package dev.krtirtho.spotube.modules.plugin
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dev.krtirtho.spotube.core.di.injectLogger
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
data class PluginDiscoverState(
val repos: List<GitHubRepo> = emptyList(),
val currentPage: Int = 1,
val hasMore: Boolean = true,
val isLoading: Boolean = false,
val isLoadingMore: Boolean = false,
val error: String? = null,
val installingRepoId: Long? = null,
val isInitialLoaded: Boolean = false,
)
class PluginDiscoverViewModel(
private val pluginManager: PluginManager,
) : ViewModel(), KoinComponent {
private val logger by injectLogger<PluginDiscoverViewModel>()
private val gitHubRepo = GitHubPluginRepository()
private val _allRepos = mutableListOf<GitHubRepo>()
private val _paginationInfo = PaginationInfo()
private val _state = MutableStateFlow(PluginDiscoverState())
val state: StateFlow<PluginDiscoverState> = _state.asStateFlow()
private class PaginationInfo(
var currentPage: Int = 1,
var hasMore: Boolean = true,
var totalCount: Int = 0,
)
init {
viewModelScope.launch {
pluginManager.state.collect { pluginState ->
if (pluginState is PluginManagerStates.Data) {
val installedUrls = pluginState.plugins.mapNotNull { it.repository.takeIf { r -> r.isNotBlank() } }.toSet()
_state.update {
it.copy(repos = _allRepos.filter { repo -> repo.htmlUrl !in installedUrls })
}
}
}
}
loadFirstPage()
}
private fun filterInstalled(repos: List<GitHubRepo>): List<GitHubRepo> {
val pluginState = pluginManager.state.value
if (pluginState !is PluginManagerStates.Data) return repos
val installedUrls = pluginState.plugins.mapNotNull { it.repository.takeIf { r -> r.isNotBlank() } }.toSet()
return repos.filter { it.htmlUrl !in installedUrls }
}
private fun loadFirstPage() {
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
runCatching {
gitHubRepo.searchSpotubePlugins(page = 1)
}.onSuccess { response ->
_allRepos.clear()
_allRepos.addAll(response.items)
_paginationInfo.currentPage = 1
_paginationInfo.totalCount = response.totalCount
_paginationInfo.hasMore = _allRepos.size < response.totalCount
_state.update {
it.copy(
repos = filterInstalled(response.items),
currentPage = 1,
hasMore = _paginationInfo.hasMore,
isLoading = false,
isInitialLoaded = true,
)
}
}.onFailure { e ->
logger.e(e) { "Failed to load plugins" }
_state.update {
it.copy(
isLoading = false,
error = e.message,
isInitialLoaded = true,
)
}
}
}
}
fun loadNextPage() {
val current = _state.value
if (current.isLoadingMore || !current.hasMore) return
viewModelScope.launch {
val nextPage = _paginationInfo.currentPage + 1
_state.update { it.copy(isLoadingMore = true, error = null) }
runCatching {
gitHubRepo.searchSpotubePlugins(page = nextPage)
}.onSuccess { response ->
_allRepos.addAll(response.items)
_paginationInfo.currentPage = nextPage
_paginationInfo.hasMore = _allRepos.size < response.totalCount
_state.update {
it.copy(
repos = filterInstalled(_allRepos),
currentPage = nextPage,
hasMore = _paginationInfo.hasMore,
isLoadingMore = false,
)
}
}.onFailure { e ->
logger.e(e) { "Failed to load more plugins" }
_state.update { it.copy(isLoadingMore = false, error = e.message) }
}
}
}
fun installPlugin(repo: GitHubRepo) {
if (_state.value.installingRepoId != null) return
_state.update { it.copy(installingRepoId = repo.id, error = null) }
viewModelScope.launch {
runCatching {
val parts = repo.fullName.split("/")
val url = gitHubRepo.getLatestReleaseSmplugUrl(parts[0], parts[1])
?: throw IllegalStateException("No .smplug asset found in latest release")
pluginManager.addPluginFromURL(url)
}.onFailure { e ->
logger.e(e) { "Failed to install plugin" }
_state.update { it.copy(error = e.message) }
}
_state.update { it.copy(installingRepoId = null) }
}
}
fun installPluginFromUrl(url: String, repoId: Long) {
if (_state.value.installingRepoId != null) return
_state.update { it.copy(installingRepoId = repoId, error = null) }
viewModelScope.launch {
runCatching {
pluginManager.addPluginFromURL(url)
}.onFailure { e ->
logger.e(e) { "Failed to install plugin" }
_state.update { it.copy(error = e.message) }
}
_state.update { it.copy(installingRepoId = null) }
}
}
suspend fun getReleases(owner: String, repo: String): List<GitHubRelease> {
return gitHubRepo.getReleases(owner, repo)
}
override fun onCleared() {
gitHubRepo.close()
super.onCleared()
}
}

View File

@ -80,7 +80,6 @@ class PluginManager(
} }
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate + pluginExceptionHandler) private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate + pluginExceptionHandler)
private val pluginsDir = "${paths.getApplicationDataDirPath()}/plugins".toPath() private val pluginsDir = "${paths.getApplicationDataDirPath()}/plugins".toPath()
val pluginsDirPath: Path get() = pluginsDir
private val httpClient = HttpClient() private val httpClient = HttpClient()
@ -325,15 +324,6 @@ class PluginManager(
throw IllegalArgumentException("Invalid plugin.json format: ${e.message}") throw IllegalArgumentException("Invalid plugin.json format: ${e.message}")
} }
// Preserve logo.png before deleting temp dir so the permission dialog can show it
val logoPngPath = tempDir / "logo.png".toPath()
if (FileSystem.SYSTEM.exists(logoPngPath)) {
val logoDir = pluginsDirPath / pluginEntry.id.toPath()
if (!FileSystem.SYSTEM.exists(logoDir)) FileSystem.SYSTEM.createDirectories(logoDir)
val destLogo = logoDir / "logo.png".toPath()
FileSystem.SYSTEM.copy(logoPngPath, destLogo)
}
pendingPlugin.value = buildPendingPlugin(pluginEntry, bytes) pendingPlugin.value = buildPendingPlugin(pluginEntry, bytes)
} catch (e: Exception) { } catch (e: Exception) {
throw Exception("Failed to read plugin: ${e.message}", e) throw Exception("Failed to read plugin: ${e.message}", e)

View File

@ -18,28 +18,20 @@
package dev.krtirtho.spotube.modules.plugin package dev.krtirtho.spotube.modules.plugin
import com.goncalossilva.murmurhash.MurmurHash3 import com.goncalossilva.murmurhash.MurmurHash3
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
enum class PluginCapability { enum class PluginCapability {
@SerialName("persistent_storage")
PERSISTENT_STORAGE, PERSISTENT_STORAGE,
@SerialName("network_requests")
NETWORK_REQUESTS, NETWORK_REQUESTS,
@SerialName("webview")
WEBVIEW WEBVIEW
} }
//Set naming strategy to snake_case for better interoperability with JavaScript plugins //Set naming strategy to snake_case for better interoperability with JavaScript plugins
@Serializable @Serializable
enum class PluginAbility { enum class PluginAbility {
@SerialName("metadata")
METADATA, METADATA,
@SerialName("audio")
AUDIO, AUDIO,
@SerialName("lyrics")
LYRICS, LYRICS,
@SerialName("scrobble")
SCROBBLE, SCROBBLE,
} }
@ -51,11 +43,7 @@ data class PluginEntry(
val description: String, val description: String,
val author: String, val author: String,
val capabilities: List<PluginCapability>, val capabilities: List<PluginCapability>,
val abilities: List<PluginAbility>, val abilities: List<PluginAbility>
val contact: String,
val repository: String,
val bugs: String,
val license: String,
) { ) {
@Suppress("REDUNDANT_CALL_OF_CONVERSION_METHOD") @Suppress("REDUNDANT_CALL_OF_CONVERSION_METHOD")
val id: String = MurmurHash3().hash32x86("$name:$author".encodeToByteArray()) val id: String = MurmurHash3().hash32x86("$name:$author".encodeToByteArray())

View File

@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
@ -39,11 +38,6 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import coil3.compose.LocalPlatformContext
import coil3.request.ImageRequest
import coil3.request.crossfade
import okio.Path
import dev.krtirtho.spotube.core.ui.base.GhostIconButton import dev.krtirtho.spotube.core.ui.base.GhostIconButton
import dev.krtirtho.spotube.core.ui.base.OutlineButton import dev.krtirtho.spotube.core.ui.base.OutlineButton
import dev.krtirtho.spotube.modules.plugin.BUILT_IN_PLUGINS import dev.krtirtho.spotube.modules.plugin.BUILT_IN_PLUGINS
@ -52,8 +46,6 @@ import dev.krtirtho.spotube.modules.plugin.PluginEntry
import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxBox import dev.krtirtho.spotube.resources.iconsax.IconsaxBox
import dev.krtirtho.spotube.resources.iconsax.IconsaxCheckSquare import dev.krtirtho.spotube.resources.iconsax.IconsaxCheckSquare
import dev.krtirtho.spotube.resources.iconsax.IconsaxInformation
import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart
import dev.krtirtho.spotube.resources.iconsax.IconsaxTag import dev.krtirtho.spotube.resources.iconsax.IconsaxTag
import dev.krtirtho.spotube.resources.iconsax.IconsaxTrash import dev.krtirtho.spotube.resources.iconsax.IconsaxTrash
import dev.krtirtho.spotube.resources.iconsax.User import dev.krtirtho.spotube.resources.iconsax.User
@ -78,9 +70,6 @@ internal fun PluginCard(
isLoggedIn: Boolean, isLoggedIn: Boolean,
onLogin: (() -> Unit)? = null, onLogin: (() -> Unit)? = null,
onLogout: (() -> Unit)? = null, onLogout: (() -> Unit)? = null,
onInfo: (() -> Unit)? = null,
onSupport: (() -> Unit)? = null,
logoPath: Path? = null,
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@ -96,25 +85,13 @@ internal fun PluginCard(
.clip(RoundedCornerShape(10.dp)), .clip(RoundedCornerShape(10.dp)),
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
) { ) {
if (logoPath != null) { Box(contentAlignment = Alignment.Center) {
val platformContext = LocalPlatformContext.current Icon(
AsyncImage( Iconsax.IconsaxBox,
model = ImageRequest.Builder(platformContext) contentDescription = null,
.data(logoPath.toString()) tint = MaterialTheme.colorScheme.primary,
.crossfade(true) modifier = Modifier.size(22.dp)
.build(),
contentDescription = plugin.name,
modifier = Modifier.fillMaxSize()
) )
} else {
Box(contentAlignment = Alignment.Center) {
Icon(
Iconsax.IconsaxBox,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(22.dp)
)
}
} }
} }
@ -238,50 +215,28 @@ internal fun PluginCard(
Column( Column(
modifier = Modifier.align(Alignment.Bottom), modifier = Modifier.align(Alignment.Bottom),
horizontalAlignment = Alignment.End, horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(4.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Row( if (plugin in BUILT_IN_PLUGINS) {
horizontalArrangement = Arrangement.spacedBy(2.dp), // Built-in plugins cannot be removed
verticalAlignment = Alignment.CenterVertically Text(
) { stringResource(Res.string.plugin_state_builtin),
if (onInfo != null) { style = MaterialTheme.typography.labelSmall,
GhostIconButton(onClick = onInfo) { color = MaterialTheme.colorScheme.onSurfaceVariant,
Icon( modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
Iconsax.IconsaxInformation, .border(
contentDescription = null, BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant),
tint = MaterialTheme.colorScheme.onSurfaceVariant, shape = RoundedCornerShape(6.dp)
) )
} .padding(horizontal = 6.dp, vertical = 2.dp)
} )
if (onSupport != null) { } else {
GhostIconButton(onClick = onSupport) { GhostIconButton(onClick = onRemove) {
Icon( Icon(
Iconsax.IconsaxHeart, Iconsax.IconsaxTrash,
contentDescription = null, contentDescription = stringResource(Res.string.plugin_action_remove),
tint = MaterialTheme.colorScheme.onSurfaceVariant, tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
if (plugin in BUILT_IN_PLUGINS) {
Text(
stringResource(Res.string.plugin_state_builtin),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
.border(
BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant),
shape = RoundedCornerShape(6.dp)
)
.padding(horizontal = 6.dp, vertical = 2.dp)
) )
} else {
GhostIconButton(onClick = onRemove) {
Icon(
Iconsax.IconsaxTrash,
contentDescription = stringResource(Res.string.plugin_action_remove),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
} }
} }

View File

@ -1,256 +0,0 @@
/*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package dev.krtirtho.spotube.modules.plugin.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import coil3.compose.LocalPlatformContext
import coil3.request.ImageRequest
import coil3.request.crossfade
import dev.krtirtho.spotube.core.ui.base.PrimaryButton
import dev.krtirtho.spotube.core.ui.base.ThemedDialog
import dev.krtirtho.spotube.modules.plugin.GitHubRepo
import dev.krtirtho.spotube.modules.plugin.GitHubRelease
import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxBox
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PluginInstallDialog(
repo: GitHubRepo,
releases: List<GitHubRelease>,
isLoadingReleases: Boolean,
onDismiss: () -> Unit,
onInstall: (GitHubRelease) -> Unit,
) {
var selectedRelease by remember { mutableStateOf<GitHubRelease?>(null) }
var expanded by remember { mutableStateOf(false) }
LaunchedEffect(releases) {
if (selectedRelease == null && releases.isNotEmpty()) {
selectedRelease = releases.first()
}
}
ThemedDialog(
onDismissRequest = onDismiss,
title = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Surface(
modifier = Modifier
.size(48.dp)
.clip(RoundedCornerShape(12.dp)),
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.12f)
) {
Box(contentAlignment = Alignment.Center) {
val platformContext = LocalPlatformContext.current
AsyncImage(
model = ImageRequest.Builder(platformContext)
.data(repo.owner.avatarUrl)
.crossfade(true)
.build(),
contentDescription = repo.owner.login,
modifier = Modifier.fillMaxSize()
)
}
}
Column(modifier = Modifier.weight(1f)) {
Text(
repo.fullName,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (!repo.description.isNullOrBlank()) {
Text(
repo.description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
}
},
actions = {
PrimaryButton(
onClick = { selectedRelease?.let { onInstall(it) } },
enabled = selectedRelease != null && !isLoadingReleases
) {
Text("Install")
}
}
) {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.fillMaxWidth()
) {
if (isLoadingReleases) {
Box(
modifier = Modifier.fillMaxWidth().height(120.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(modifier = Modifier.size(32.dp))
}
} else if (releases.isEmpty()) {
Text(
"No releases found",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
} else {
Text(
"Select Release",
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurface
)
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = it }
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.menuAnchor(MenuAnchorType.PrimaryNotEditable),
shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colorScheme.surfaceVariant
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
selectedRelease?.tagName ?: "Select a release",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface
)
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
}
}
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
releases.forEach { release ->
DropdownMenuItem(
text = {
Column {
Text(
release.tagName,
style = MaterialTheme.typography.bodyMedium,
fontWeight = if (release == selectedRelease) FontWeight.SemiBold else FontWeight.Normal
)
if (release.prerelease) {
Text(
"Pre-release",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.error
)
}
}
},
onClick = {
selectedRelease = release
expanded = false
}
)
}
}
}
selectedRelease?.let { release ->
if (!release.body.isNullOrBlank()) {
Text(
"Release Notes",
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurface
)
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 200.dp)
.verticalScroll(rememberScrollState())
.padding(12.dp)
) {
Text(
release.body,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
}
}
}

View File

@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
@ -39,11 +38,6 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import coil3.compose.LocalPlatformContext
import coil3.request.ImageRequest
import coil3.request.crossfade
import okio.Path
import dev.krtirtho.spotube.core.ui.base.Card import dev.krtirtho.spotube.core.ui.base.Card
import dev.krtirtho.spotube.core.ui.base.OutlineButton import dev.krtirtho.spotube.core.ui.base.OutlineButton
import dev.krtirtho.spotube.core.ui.base.PrimaryButton import dev.krtirtho.spotube.core.ui.base.PrimaryButton
@ -82,7 +76,6 @@ fun PluginPermissionDialog(
message: String, message: String,
confirmLabel: String?, confirmLabel: String?,
existingPlugin: PluginEntry? = null, existingPlugin: PluginEntry? = null,
logoPath: Path? = null,
onConfirm: (() -> Unit)? = null, onConfirm: (() -> Unit)? = null,
onDismiss: () -> Unit, onDismiss: () -> Unit,
) { ) {
@ -100,24 +93,12 @@ fun PluginPermissionDialog(
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.12f)), .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.12f)),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
if (logoPath != null) { Icon(
val platformContext = LocalPlatformContext.current imageVector = Iconsax.IconsaxBoxAdd,
AsyncImage( contentDescription = null,
model = ImageRequest.Builder(platformContext) tint = MaterialTheme.colorScheme.primary,
.data(logoPath.toString()) modifier = Modifier.size(24.dp)
.crossfade(true) )
.build(),
contentDescription = pluginInfo.name,
modifier = Modifier.fillMaxSize()
)
} else {
Icon(
imageVector = Iconsax.IconsaxBoxAdd,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp)
)
}
} }
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(

View File

@ -1,57 +0,0 @@
package dev.krtirtho.spotube.resources.iconsax
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
val Iconsax.CarbonGithubLogo: ImageVector
get() {
if (_CarbonGithubLogo != null) {
return _CarbonGithubLogo!!
}
_CarbonGithubLogo = ImageVector.Builder(
name = "CarbonGithubLogo",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 32f,
viewportHeight = 32f
).apply {
path(
fill = SolidColor(Color.Black),
stroke = SolidColor(Color.Black),
strokeLineWidth = 1f
) {
moveTo(16f, 2f)
arcToRelative(14f, 14f, 0f, isMoreThanHalf = false, isPositiveArc = false, -4.43f, 27.28f)
curveToRelative(0.7f, 0.13f, 1f, -0.3f, 1f, -0.67f)
reflectiveCurveToRelative(0f, -1.21f, 0f, -2.38f)
curveToRelative(-3.89f, 0.84f, -4.71f, -1.88f, -4.71f, -1.88f)
arcTo(3.71f, 3.71f, 0f, isMoreThanHalf = false, isPositiveArc = false, 6.24f, 22.3f)
curveToRelative(-1.27f, -0.86f, 0.1f, -0.85f, 0.1f, -0.85f)
arcTo(2.94f, 2.94f, 0f, isMoreThanHalf = false, isPositiveArc = true, 8.48f, 22.9f)
arcToRelative(3f, 3f, 0f, isMoreThanHalf = false, isPositiveArc = false, 4.08f, 1.16f)
arcToRelative(2.93f, 2.93f, 0f, isMoreThanHalf = false, isPositiveArc = true, 0.88f, -1.87f)
curveToRelative(-3.1f, -0.36f, -6.37f, -1.56f, -6.37f, -6.92f)
arcToRelative(5.4f, 5.4f, 0f, isMoreThanHalf = false, isPositiveArc = true, 1.44f, -3.76f)
arcToRelative(5f, 5f, 0f, isMoreThanHalf = false, isPositiveArc = true, 0.14f, -3.7f)
reflectiveCurveToRelative(1.17f, -0.38f, 3.85f, 1.43f)
arcToRelative(13.3f, 13.3f, 0f, isMoreThanHalf = false, isPositiveArc = true, 7f, 0f)
curveToRelative(2.67f, -1.81f, 3.84f, -1.43f, 3.84f, -1.43f)
arcToRelative(5f, 5f, 0f, isMoreThanHalf = false, isPositiveArc = true, 0.14f, 3.7f)
arcToRelative(5.4f, 5.4f, 0f, isMoreThanHalf = false, isPositiveArc = true, 1.44f, 3.76f)
curveToRelative(0f, 5.38f, -3.27f, 6.56f, -6.39f, 6.91f)
arcToRelative(3.33f, 3.33f, 0f, isMoreThanHalf = false, isPositiveArc = true, 0.95f, 2.59f)
curveToRelative(0f, 1.87f, 0f, 3.38f, 0f, 3.84f)
reflectiveCurveToRelative(0.25f, 0.81f, 1f, 0.67f)
arcTo(14f, 14f, 0f, isMoreThanHalf = false, isPositiveArc = false, 16f, 2f)
close()
}
}.build()
return _CarbonGithubLogo!!
}
@Suppress("ObjectPropertyName")
private var _CarbonGithubLogo: ImageVector? = null

View File

@ -1,97 +0,0 @@
package dev.krtirtho.spotube.resources.iconsax
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.PathData
import androidx.compose.ui.graphics.vector.group
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
val Iconsax.IconsaxInformation: ImageVector
get() {
if (_IconsaxInformation != null) {
return _IconsaxInformation!!
}
_IconsaxInformation = ImageVector.Builder(
name = "IconsaxInformation",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f
).apply {
group(
clipPathData = PathData {
moveTo(0f, 0f)
horizontalLineToRelative(24f)
verticalLineToRelative(24f)
horizontalLineToRelative(-24f)
close()
}
) {
path(
fill = SolidColor(Color.White),
fillAlpha = 0.4f,
strokeAlpha = 0.4f
) {
moveTo(10.75f, 2.45f)
curveTo(11.45f, 1.86f, 12.58f, 1.86f, 13.26f, 2.45f)
lineTo(14.84f, 3.8f)
curveTo(15.14f, 4.05f, 15.71f, 4.26f, 16.11f, 4.26f)
horizontalLineTo(17.81f)
curveTo(18.87f, 4.26f, 19.74f, 5.13f, 19.74f, 6.19f)
verticalLineTo(7.89f)
curveTo(19.74f, 8.29f, 19.95f, 8.85f, 20.2f, 9.15f)
lineTo(21.55f, 10.73f)
curveTo(22.14f, 11.43f, 22.14f, 12.56f, 21.55f, 13.24f)
lineTo(20.2f, 14.82f)
curveTo(19.95f, 15.12f, 19.74f, 15.68f, 19.74f, 16.08f)
verticalLineTo(17.78f)
curveTo(19.74f, 18.84f, 18.87f, 19.71f, 17.81f, 19.71f)
horizontalLineTo(16.11f)
curveTo(15.71f, 19.71f, 15.15f, 19.92f, 14.85f, 20.17f)
lineTo(13.27f, 21.52f)
curveTo(12.57f, 22.11f, 11.44f, 22.11f, 10.76f, 21.52f)
lineTo(9.18f, 20.17f)
curveTo(8.88f, 19.92f, 8.31f, 19.71f, 7.92f, 19.71f)
horizontalLineTo(6.17f)
curveTo(5.11f, 19.71f, 4.24f, 18.84f, 4.24f, 17.78f)
verticalLineTo(16.07f)
curveTo(4.24f, 15.68f, 4.04f, 15.11f, 3.79f, 14.82f)
lineTo(2.44f, 13.23f)
curveTo(1.86f, 12.54f, 1.86f, 11.42f, 2.44f, 10.73f)
lineTo(3.79f, 9.14f)
curveTo(4.04f, 8.84f, 4.24f, 8.28f, 4.24f, 7.89f)
verticalLineTo(6.2f)
curveTo(4.24f, 5.14f, 5.11f, 4.27f, 6.17f, 4.27f)
horizontalLineTo(7.9f)
curveTo(8.3f, 4.27f, 8.86f, 4.06f, 9.16f, 3.81f)
lineTo(10.75f, 2.45f)
close()
}
path(fill = SolidColor(Color.White)) {
moveTo(12f, 16.869f)
curveTo(11.45f, 16.869f, 11f, 16.419f, 11f, 15.869f)
curveTo(11f, 15.319f, 11.44f, 14.869f, 12f, 14.869f)
curveTo(12.55f, 14.869f, 13f, 15.319f, 13f, 15.869f)
curveTo(13f, 16.419f, 12.56f, 16.869f, 12f, 16.869f)
close()
}
path(fill = SolidColor(Color.White)) {
moveTo(12f, 13.721f)
curveTo(11.59f, 13.721f, 11.25f, 13.381f, 11.25f, 12.971f)
verticalLineTo(8.131f)
curveTo(11.25f, 7.721f, 11.59f, 7.381f, 12f, 7.381f)
curveTo(12.41f, 7.381f, 12.75f, 7.721f, 12.75f, 8.131f)
verticalLineTo(12.961f)
curveTo(12.75f, 13.381f, 12.42f, 13.721f, 12f, 13.721f)
close()
}
}
}.build()
return _IconsaxInformation!!
}
@Suppress("ObjectPropertyName")
private var _IconsaxInformation: ImageVector? = null

View File

@ -1,138 +0,0 @@
use discord_rich_presence::activity::{Activity, ActivityType, Assets, Timestamps};
use discord_rich_presence::{DiscordIpc, DiscordIpcClient};
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, uniffi::Error)]
pub enum DiscordRpcError {
ConnectionError { reason: String },
UpdateError { reason: String },
NotConnected,
}
impl std::fmt::Display for DiscordRpcError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DiscordRpcError::ConnectionError { reason } => write!(f, "Connection error: {}", reason),
DiscordRpcError::UpdateError { reason } => write!(f, "Update error: {}", reason),
DiscordRpcError::NotConnected => write!(f, "Not connected"),
}
}
}
#[derive(uniffi::Object)]
pub struct DiscordRpcClient {
client_id: String,
client: Mutex<Option<DiscordIpcClient>>,
}
#[uniffi::export]
impl DiscordRpcClient {
#[uniffi::constructor]
pub fn new(client_id: String) -> Self {
Self {
client_id,
client: Mutex::new(None),
}
}
pub fn connect(&self) -> Result<(), DiscordRpcError> {
let mut client_guard = self.client.lock().unwrap();
if client_guard.is_some() {
return Ok(());
}
let mut client = DiscordIpcClient::new(&self.client_id);
client
.connect()
.map_err(|e| DiscordRpcError::ConnectionError { reason: e.to_string() })?;
*client_guard = Some(client);
Ok(())
}
pub fn disconnect(&self) -> Result<(), DiscordRpcError> {
let mut client_guard = self.client.lock().unwrap();
if let Some(mut client) = client_guard.take() {
client
.close()
.map_err(|e| DiscordRpcError::ConnectionError { reason: e.to_string() })?;
}
Ok(())
}
pub fn update_presence(
&self,
title: String,
artist: String,
album: String,
cover_url: String,
position_ms: i64,
duration_ms: i64,
) -> Result<(), DiscordRpcError> {
let mut client_guard = self.client.lock().unwrap();
let client = client_guard
.as_mut()
.ok_or(DiscordRpcError::NotConnected)?;
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let start = now - (position_ms / 1000);
let end = start + (duration_ms / 1000);
let details = if title.is_empty() {
"Unknown Track".to_string()
} else {
title
};
let state = if artist.is_empty() {
"Unknown Artist".to_string()
} else {
format!("by {}", artist)
};
let mut activity = Activity::new()
.details(&details)
.state(&state)
.timestamps(Timestamps::new().start(start).end(end))
.activity_type(ActivityType::Listening);
if !album.is_empty() {
let mut assets = Assets::new().large_text(&album);
if !cover_url.is_empty() {
assets = assets.large_image(&cover_url);
}
activity = activity.assets(assets);
} else if !cover_url.is_empty() {
activity = activity.assets(Assets::new().large_image(&cover_url));
}
client
.set_activity(activity)
.map_err(|e| DiscordRpcError::UpdateError { reason: e.to_string() })?;
Ok(())
}
pub fn clear_presence(&self) -> Result<(), DiscordRpcError> {
let mut client_guard = self.client.lock().unwrap();
let client = client_guard
.as_mut()
.ok_or(DiscordRpcError::NotConnected)?;
client
.clear_activity()
.map_err(|e| DiscordRpcError::UpdateError { reason: e.to_string() })?;
Ok(())
}
pub fn is_connected(&self) -> bool {
self.client.lock().unwrap().is_some()
}
}

View File

@ -1,7 +1,298 @@
mod metadata; use discord_rich_presence::activity::{Activity, ActivityType, Assets, Timestamps};
mod discord_rpc; use discord_rich_presence::{DiscordIpc, DiscordIpcClient};
use lofty::config::WriteOptions;
use lofty::file::{AudioFile, FileType, TaggedFileExt};
use lofty::picture::Picture;
use lofty::probe::Probe;
use lofty::tag::{Accessor, Tag, TagExt};
use std::path::Path;
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
pub use metadata::*; #[derive(uniffi::Record)]
pub use discord_rpc::*; pub struct AudioMetadata {
pub title: Option<String>,
pub artists: Vec<String>,
pub album: Option<String>,
pub duration_ms: i64,
pub track_number: Option<u32>,
pub disc_number: Option<u32>,
pub cover_bytes: Option<Vec<u8>>,
}
#[derive(Debug, uniffi::Error)]
pub enum AudioTagError {
FileNotFound,
ReadError { reason: String },
WriteError { reason: String },
}
impl std::fmt::Display for AudioTagError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AudioTagError::FileNotFound => write!(f, "File not found"),
AudioTagError::ReadError { reason } => write!(f, "Read error: {}", reason),
AudioTagError::WriteError { reason } => write!(f, "Write error: {}", reason),
}
}
}
#[uniffi::export]
fn read_audio_metadata(file_path: String) -> Result<AudioMetadata, AudioTagError> {
let path = Path::new(&file_path);
if !path.exists() {
return Err(AudioTagError::FileNotFound);
}
let tagged_file = Probe::open(path)
.map_err(|e| AudioTagError::ReadError { reason: e.to_string() })?
.read()
.map_err(|e| AudioTagError::ReadError { reason: e.to_string() })?;
let duration = tagged_file.properties().duration();
let duration_ms = duration.as_millis() as i64;
let tag = tagged_file.primary_tag().or_else(|| tagged_file.first_tag());
let mut title = None;
let mut artists = Vec::new();
let mut album = None;
let mut track_number = None;
let mut disc_number = None;
let mut cover_bytes = None;
if let Some(tag) = tag {
title = tag.title().map(|s| s.to_string());
if let Some(artist) = tag.artist() {
artists = artist
.split(';')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
}
album = tag.album().map(|s| s.to_string());
track_number = tag.track();
disc_number = tag.disk();
if let Some(pic) = tag.pictures().first() {
cover_bytes = Some(pic.data().to_vec());
}
}
Ok(AudioMetadata {
title,
artists,
album,
duration_ms,
track_number,
disc_number,
cover_bytes,
})
}
#[uniffi::export]
fn write_audio_metadata(
file_path: String,
title: String,
artists: String,
album: Option<String>,
track_number: Option<i32>,
disc_number: Option<i32>,
cover_bytes: Option<Vec<u8>>,
) -> Result<(), AudioTagError> {
let path = Path::new(&file_path);
if !path.exists() {
return Err(AudioTagError::FileNotFound);
}
let mut tagged_file = Probe::open(path)
.map_err(|e| AudioTagError::ReadError { reason: e.to_string() })?
.read()
.map_err(|e| AudioTagError::ReadError { reason: e.to_string() })?;
let file_type = tagged_file.file_type();
if tagged_file.primary_tag().is_none() && tagged_file.first_tag().is_none() {
let new_tag = match file_type {
FileType::Mpeg => Tag::new(lofty::tag::TagType::Id3v2),
FileType::Mp4 => Tag::new(lofty::tag::TagType::Mp4Ilst),
FileType::Flac => Tag::new(lofty::tag::TagType::VorbisComments),
FileType::Opus | FileType::Vorbis => Tag::new(lofty::tag::TagType::VorbisComments),
_ => Tag::new(lofty::tag::TagType::Id3v2),
};
tagged_file.insert_tag(new_tag);
}
let tag = if tagged_file.primary_tag().is_some() {
tagged_file.primary_tag_mut().unwrap()
} else if tagged_file.first_tag().is_some() {
tagged_file.first_tag_mut().unwrap()
} else {
unreachable!()
};
tag.set_title(title);
tag.set_artist(artists);
if let Some(album_name) = album {
tag.set_album(album_name);
}
if let Some(num) = track_number {
tag.set_track(num as u32);
}
if let Some(num) = disc_number {
tag.set_disk(num as u32);
}
if let Some(bytes) = cover_bytes {
if !bytes.is_empty() {
let picture = Picture::unchecked(bytes).build();
tag.push_picture(picture);
}
}
tag.save_to_path(path, WriteOptions::default())
.map_err(|e| AudioTagError::WriteError { reason: e.to_string() })?;
Ok(())
}
#[derive(Debug, uniffi::Error)]
pub enum DiscordRpcError {
ConnectionError { reason: String },
UpdateError { reason: String },
NotConnected,
}
impl std::fmt::Display for DiscordRpcError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DiscordRpcError::ConnectionError { reason } => write!(f, "Connection error: {}", reason),
DiscordRpcError::UpdateError { reason } => write!(f, "Update error: {}", reason),
DiscordRpcError::NotConnected => write!(f, "Not connected"),
}
}
}
#[derive(uniffi::Object)]
pub struct DiscordRpcClient {
client_id: String,
client: Mutex<Option<DiscordIpcClient>>,
}
#[uniffi::export]
impl DiscordRpcClient {
#[uniffi::constructor]
pub fn new(client_id: String) -> Self {
Self {
client_id,
client: Mutex::new(None),
}
}
pub fn connect(&self) -> Result<(), DiscordRpcError> {
let mut client_guard = self.client.lock().unwrap();
if client_guard.is_some() {
return Ok(());
}
let mut client = DiscordIpcClient::new(&self.client_id);
client
.connect()
.map_err(|e| DiscordRpcError::ConnectionError { reason: e.to_string() })?;
*client_guard = Some(client);
Ok(())
}
pub fn disconnect(&self) -> Result<(), DiscordRpcError> {
let mut client_guard = self.client.lock().unwrap();
if let Some(mut client) = client_guard.take() {
client
.close()
.map_err(|e| DiscordRpcError::ConnectionError { reason: e.to_string() })?;
}
Ok(())
}
pub fn update_presence(
&self,
title: String,
artist: String,
album: String,
cover_url: String,
position_ms: i64,
duration_ms: i64,
) -> Result<(), DiscordRpcError> {
let mut client_guard = self.client.lock().unwrap();
let client = client_guard
.as_mut()
.ok_or(DiscordRpcError::NotConnected)?;
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let start = now - (position_ms / 1000);
let end = start + (duration_ms / 1000);
let details = if title.is_empty() {
"Unknown Track".to_string()
} else {
title
};
let state = if artist.is_empty() {
"Unknown Artist".to_string()
} else {
format!("by {}", artist)
};
let mut activity = Activity::new()
.details(&details)
.state(&state)
.timestamps(Timestamps::new().start(start).end(end))
.activity_type(ActivityType::Listening);
if !album.is_empty() {
let mut assets = Assets::new().large_text(&album);
if !cover_url.is_empty() {
assets = assets.large_image(&cover_url);
}
activity = activity.assets(assets);
} else if !cover_url.is_empty() {
activity = activity.assets(Assets::new().large_image(&cover_url));
}
client
.set_activity(activity)
.map_err(|e| DiscordRpcError::UpdateError { reason: e.to_string() })?;
Ok(())
}
pub fn clear_presence(&self) -> Result<(), DiscordRpcError> {
let mut client_guard = self.client.lock().unwrap();
let client = client_guard
.as_mut()
.ok_or(DiscordRpcError::NotConnected)?;
client
.clear_activity()
.map_err(|e| DiscordRpcError::UpdateError { reason: e.to_string() })?;
Ok(())
}
pub fn is_connected(&self) -> bool {
self.client.lock().unwrap().is_some()
}
}
uniffi::setup_scaffolding!(); uniffi::setup_scaffolding!();

View File

@ -1,158 +0,0 @@
use lofty::config::WriteOptions;
use lofty::file::{AudioFile, FileType, TaggedFileExt};
use lofty::picture::Picture;
use lofty::probe::Probe;
use lofty::tag::{Accessor, Tag, TagExt};
use std::path::Path;
#[derive(uniffi::Record)]
pub struct AudioMetadata {
pub title: Option<String>,
pub artists: Vec<String>,
pub album: Option<String>,
pub duration_ms: i64,
pub track_number: Option<u32>,
pub disc_number: Option<u32>,
pub cover_bytes: Option<Vec<u8>>,
}
#[derive(Debug, uniffi::Error)]
pub enum AudioTagError {
FileNotFound,
ReadError { reason: String },
WriteError { reason: String },
}
impl std::fmt::Display for AudioTagError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AudioTagError::FileNotFound => write!(f, "File not found"),
AudioTagError::ReadError { reason } => write!(f, "Read error: {}", reason),
AudioTagError::WriteError { reason } => write!(f, "Write error: {}", reason),
}
}
}
#[uniffi::export]
pub fn read_audio_metadata(file_path: String) -> Result<AudioMetadata, AudioTagError> {
let path = Path::new(&file_path);
if !path.exists() {
return Err(AudioTagError::FileNotFound);
}
let tagged_file = Probe::open(path)
.map_err(|e| AudioTagError::ReadError { reason: e.to_string() })?
.read()
.map_err(|e| AudioTagError::ReadError { reason: e.to_string() })?;
let duration = tagged_file.properties().duration();
let duration_ms = duration.as_millis() as i64;
let tag = tagged_file.primary_tag().or_else(|| tagged_file.first_tag());
let mut title = None;
let mut artists = Vec::new();
let mut album = None;
let mut track_number = None;
let mut disc_number = None;
let mut cover_bytes = None;
if let Some(tag) = tag {
title = tag.title().map(|s| s.to_string());
if let Some(artist) = tag.artist() {
artists = artist
.split(';')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
}
album = tag.album().map(|s| s.to_string());
track_number = tag.track();
disc_number = tag.disk();
if let Some(pic) = tag.pictures().first() {
cover_bytes = Some(pic.data().to_vec());
}
}
Ok(AudioMetadata {
title,
artists,
album,
duration_ms,
track_number,
disc_number,
cover_bytes,
})
}
#[uniffi::export]
pub fn write_audio_metadata(
file_path: String,
title: String,
artists: String,
album: Option<String>,
track_number: Option<i32>,
disc_number: Option<i32>,
cover_bytes: Option<Vec<u8>>,
) -> Result<(), AudioTagError> {
let path = Path::new(&file_path);
if !path.exists() {
return Err(AudioTagError::FileNotFound);
}
let mut tagged_file = Probe::open(path)
.map_err(|e| AudioTagError::ReadError { reason: e.to_string() })?
.read()
.map_err(|e| AudioTagError::ReadError { reason: e.to_string() })?;
let file_type = tagged_file.file_type();
if tagged_file.primary_tag().is_none() && tagged_file.first_tag().is_none() {
let new_tag = match file_type {
FileType::Mpeg => Tag::new(lofty::tag::TagType::Id3v2),
FileType::Mp4 => Tag::new(lofty::tag::TagType::Mp4Ilst),
FileType::Flac => Tag::new(lofty::tag::TagType::VorbisComments),
FileType::Opus | FileType::Vorbis => Tag::new(lofty::tag::TagType::VorbisComments),
_ => Tag::new(lofty::tag::TagType::Id3v2),
};
tagged_file.insert_tag(new_tag);
}
let tag = if tagged_file.primary_tag().is_some() {
tagged_file.primary_tag_mut().unwrap()
} else if tagged_file.first_tag().is_some() {
tagged_file.first_tag_mut().unwrap()
} else {
unreachable!()
};
tag.set_title(title);
tag.set_artist(artists);
if let Some(album_name) = album {
tag.set_album(album_name);
}
if let Some(num) = track_number {
tag.set_track(num as u32);
}
if let Some(num) = disc_number {
tag.set_disk(num as u32);
}
if let Some(bytes) = cover_bytes {
if !bytes.is_empty() {
let picture = Picture::unchecked(bytes).build();
tag.push_picture(picture);
}
}
tag.save_to_path(path, WriteOptions::default())
.map_err(|e| AudioTagError::WriteError { reason: e.to_string() })?;
Ok(())
}

View File

@ -17,8 +17,6 @@
package dev.krtirtho.spotube package dev.krtirtho.spotube
import platform.Foundation.NSURL
import platform.UIKit.UIApplication
import platform.UIKit.UIDevice import platform.UIKit.UIDevice
class IOSPlatform: Platform { class IOSPlatform: Platform {
@ -26,9 +24,4 @@ class IOSPlatform: Platform {
override val type: PlatformType = PlatformType.IOS override val type: PlatformType = PlatformType.IOS
} }
actual fun getPlatform(): Platform = IOSPlatform() actual fun getPlatform(): Platform = IOSPlatform()
actual fun openUrlInBrowser(url: String) {
val nsUrl = NSURL.URLWithString(url) ?: return
UIApplication.sharedApplication.openURL(nsUrl)
}

View File

@ -1,41 +1,23 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by * it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details. * GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.webview package dev.krtirtho.spotube.core.webview
import io.github.kdroidfilter.webview.web.WebViewState import io.github.kdroidfilter.webview.web.WebViewState
import kotlinx.cinterop.ExperimentalForeignApi
import platform.Foundation.NSDate actual fun platformWebviewConfig(webView: WebViewState) {
import platform.Foundation.NSHTTPCookieStorage
import platform.Foundation.distantPast
import platform.WebKit.WKWebsiteDataStore
import platform.WebKit.WKWebsiteDataTypeCookies
import platform.WebKit.WKWebsiteDataTypeLocalStorage
actual fun platformWebviewConfig(webView: WebViewState, pluginId: String?) {
}
@OptIn(ExperimentalForeignApi::class)
actual suspend fun platformClearWebviewData(pluginId: String?) {
if (pluginId == null) return
val dataStore = WKWebsiteDataStore.defaultDataStore()
val dataTypes = setOf(WKWebsiteDataTypeCookies, WKWebsiteDataTypeLocalStorage)
dataStore.removeDataOfTypes(dataTypes, NSDate.distantPast) {}
NSHTTPCookieStorage.sharedHTTPCookieStorage.removeCookiesSinceDate(NSDate.distantPast)
} }

View File

@ -17,9 +17,6 @@
package dev.krtirtho.spotube package dev.krtirtho.spotube
import java.awt.Desktop
import java.net.URI
class JVMPlatform : Platform { class JVMPlatform : Platform {
override val name: String = "Java ${System.getProperty("java.version")}" override val name: String = "Java ${System.getProperty("java.version")}"
override val type: PlatformType = System.getProperty("os.name").let { osName -> override val type: PlatformType = System.getProperty("os.name").let { osName ->
@ -36,8 +33,4 @@ class JVMPlatform : Platform {
} }
} }
actual fun getPlatform(): Platform = JVMPlatform() actual fun getPlatform(): Platform = JVMPlatform()
actual fun openUrlInBrowser(url: String) {
Desktop.getDesktop().browse(URI(url))
}

View File

@ -1,42 +1,31 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by * it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details. * GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.webview package dev.krtirtho.spotube.core.webview
import dev.krtirtho.spotube.core.paths.Paths import dev.krtirtho.spotube.core.paths.Paths
import io.github.kdroidfilter.webview.web.WebViewState import io.github.kdroidfilter.webview.web.WebViewState
import okio.FileSystem import io.github.vinceglb.filekit.utils.div
import okio.Path.Companion.toPath import io.github.vinceglb.filekit.utils.toPath
import org.koin.core.context.GlobalContext import org.koin.core.context.GlobalContext
actual fun platformWebviewConfig(webView: WebViewState, pluginId: String?) { actual fun platformWebviewConfig(webView: WebViewState) {
val paths = GlobalContext.get().get<Paths>() val paths = GlobalContext.get().get<Paths>()
val baseDir = "${paths.getApplicationCacheDirPath()}/webview_data".toPath() webView.webSettings.desktopWebSettings.dataDirectory =
val dataDir = if (pluginId != null) baseDir / pluginId else baseDir (paths.getApplicationCacheDirPath().toPath() / "webview_data").toString()
webView.webSettings.desktopWebSettings.dataDirectory = dataDir.toString()
}
actual suspend fun platformClearWebviewData(pluginId: String?) {
if (pluginId == null) return
val paths = GlobalContext.get().get<Paths>()
val dataDirStr = "${paths.getApplicationCacheDirPath()}/webview_data/$pluginId"
val dataDir = dataDirStr.toPath()
if (FileSystem.SYSTEM.exists(dataDir)) {
FileSystem.SYSTEM.deleteRecursively(dataDir)
}
} }

201
js_plugin_example/LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.js_plugin_example package dev.krtirtho.js_plugin_example

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.js_plugin_example.plugin_apis.audio package dev.krtirtho.js_plugin_example.plugin_apis.audio

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.js_plugin_example.plugin_apis.core package dev.krtirtho.js_plugin_example.plugin_apis.core

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.js_plugin_example.plugin_apis.lyrics package dev.krtirtho.js_plugin_example.plugin_apis.lyrics

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.js_plugin_example.plugin_apis.metadata package dev.krtirtho.js_plugin_example.plugin_apis.metadata

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.js_plugin_example.plugin_apis.metadata package dev.krtirtho.js_plugin_example.plugin_apis.metadata

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.js_plugin_example.plugin_apis.metadata package dev.krtirtho.js_plugin_example.plugin_apis.metadata

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.js_plugin_example.plugin_apis.metadata package dev.krtirtho.js_plugin_example.plugin_apis.metadata

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.js_plugin_example.plugin_apis.metadata package dev.krtirtho.js_plugin_example.plugin_apis.metadata

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.js_plugin_example.plugin_apis.metadata package dev.krtirtho.js_plugin_example.plugin_apis.metadata

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.js_plugin_example.plugin_apis.metadata package dev.krtirtho.js_plugin_example.plugin_apis.metadata

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.js_plugin_example.plugin_apis.metadata package dev.krtirtho.js_plugin_example.plugin_apis.metadata

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.js_plugin_example.plugin_apis.scrobble package dev.krtirtho.js_plugin_example.plugin_apis.scrobble

View File

@ -1,23 +1,31 @@
# Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors # Copyright 2026 Kingkor Roy Tirtho and Spotube Contributors
# #
# This program is free software: you can redistribute it and/or modify # Licensed under the Apache License, Version 2.0 (the "License");
# it under the terms of the GNU Affero General Public License as published by # you may not use this file except in compliance with the License.
# the Free Software Foundation, either version 3 of the License, or # You may obtain a copy of the License at
# (at your option) any later version.
# #
# This program is distributed in the hope that it will be useful, # http://www.apache.org/licenses/LICENSE-2.0
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# #
# You should have received a copy of the GNU Affero General Public License # Unless required by applicable law or agreed to in writing, software
# along with this program. If not, see <https://www.gnu.org/licenses/>. # distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
pre-commit: pre-commit:
parallel: true parallel: true
commands: commands:
# 1. Protect the AGPL Core (Exclude the Apache libraries) # 1. Protect the AGPL Core (Exclude the Apache libraries)
agpl-headers: agpl-headers:
glob: "*.{kt,kts,xml,yaml,yml}" glob: "*.{kt,kts,xml}"
exclude: "(js_plugin_example|plugin_interfaces)/"
run: addlicense -f .github/agpl_header.txt {staged_files} run: addlicense -f .github/agpl_header.txt {staged_files}
stage_fixed: true
# 2. Protect the Apache Libraries (Only include those folders)
apache-headers:
glob: "(js_plugin_example|plugin_interfaces)/**/*.{kt,kts,xml}"
run: addlicense -f .github/apache_header.txt {staged_files}
stage_fixed: true stage_fixed: true

201
plugin_interfaces/LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.core package dev.krtirtho.plugin_interfaces.core

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.core.browser_apis package dev.krtirtho.plugin_interfaces.core.browser_apis

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.extras.logger package dev.krtirtho.plugin_interfaces.extras.logger

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.core package dev.krtirtho.plugin_interfaces.core

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.core.browser_apis package dev.krtirtho.plugin_interfaces.core.browser_apis

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.extras.logger package dev.krtirtho.plugin_interfaces.extras.logger

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.extras.spotor package dev.krtirtho.plugin_interfaces.extras.spotor

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.extras.spotor package dev.krtirtho.plugin_interfaces.extras.spotor

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.extras.spotor package dev.krtirtho.plugin_interfaces.extras.spotor

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.extras.spotor package dev.krtirtho.plugin_interfaces.extras.spotor

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.extras.spotor package dev.krtirtho.plugin_interfaces.extras.spotor

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.extras.spotor package dev.krtirtho.plugin_interfaces.extras.spotor

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.extras.spotor package dev.krtirtho.plugin_interfaces.extras.spotor

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.extras.spotor package dev.krtirtho.plugin_interfaces.extras.spotor

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.host_apis package dev.krtirtho.plugin_interfaces.host_apis

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.host_apis package dev.krtirtho.plugin_interfaces.host_apis

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.host_apis package dev.krtirtho.plugin_interfaces.host_apis

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.host_apis package dev.krtirtho.plugin_interfaces.host_apis

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.host_apis package dev.krtirtho.plugin_interfaces.host_apis

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.plugin_apis.audio package dev.krtirtho.plugin_interfaces.plugin_apis.audio

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.plugin_apis.audio package dev.krtirtho.plugin_interfaces.plugin_apis.audio

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.plugin_apis.core package dev.krtirtho.plugin_interfaces.plugin_apis.core

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.plugin_apis.core package dev.krtirtho.plugin_interfaces.plugin_apis.core

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.plugin_apis.lyrics package dev.krtirtho.plugin_interfaces.plugin_apis.lyrics

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.plugin_apis.lyrics package dev.krtirtho.plugin_interfaces.plugin_apis.lyrics

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.album package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.album

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.album package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.album

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.artist package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.artist

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.artist package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.artist

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.browse package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.browse

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.browse package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.browse

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.common package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.common

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.playlist package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.playlist

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.playlist package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.playlist

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.search package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.search

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.search package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.search

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.user package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.user

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.user package dev.krtirtho.plugin_interfaces.plugin_apis.metadata.user

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.plugin_apis.scrobble package dev.krtirtho.plugin_interfaces.plugin_apis.scrobble

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.plugin_apis.scrobble package dev.krtirtho.plugin_interfaces.plugin_apis.scrobble

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.core package dev.krtirtho.plugin_interfaces.core

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.core.browser_apis package dev.krtirtho.plugin_interfaces.core.browser_apis

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.extras.logger package dev.krtirtho.plugin_interfaces.extras.logger

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.core package dev.krtirtho.plugin_interfaces.core

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.core.browser_apis package dev.krtirtho.plugin_interfaces.core.browser_apis

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.core.browser_apis package dev.krtirtho.plugin_interfaces.core.browser_apis

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.extras.logger package dev.krtirtho.plugin_interfaces.extras.logger

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.core package dev.krtirtho.plugin_interfaces.core

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.core.browser_apis package dev.krtirtho.plugin_interfaces.core.browser_apis

View File

@ -1,18 +1,17 @@
/* /*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
* *
* This program is free software: you can redistribute it and/or modify * Licensed under the Apache License, Version 2.0 (the "License");
* it under the terms of the GNU Affero General Public License as published by * you may not use this file except in compliance with the License.
* the Free Software Foundation, either version 3 of the License, or * You may obtain a copy of the License at
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * http://www.apache.org/licenses/LICENSE-2.0
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * Unless required by applicable law or agreed to in writing, software
* along with this program. If not, see <https://www.gnu.org/licenses/>. * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package dev.krtirtho.plugin_interfaces.extras.logger package dev.krtirtho.plugin_interfaces.extras.logger