diff --git a/src/main/kotlin/com/h0tk3y/player/Main.kt b/src/main/kotlin/com/h0tk3y/player/Main.kt index 7b1ee03..2532493 100644 --- a/src/main/kotlin/com/h0tk3y/player/Main.kt +++ b/src/main/kotlin/com/h0tk3y/player/Main.kt @@ -12,7 +12,8 @@ fun main(args: Array) { ConsolePlaybackReporterPlugin::class.java.canonicalName, ConsoleControlsPlugin::class.java.canonicalName, StaticPlaylistsLibraryContributor::class.java.canonicalName, - "com.h0tk3y.third.party.plugin.UsageStatsPlugin" + "com.h0tk3y.third.party.plugin.UsageStatsPlugin", + "com.h0tk3y.third.party.plugin.AdsPlugin" ) ).init() } \ No newline at end of file diff --git a/src/main/kotlin/com/h0tk3y/player/MusicApp.kt b/src/main/kotlin/com/h0tk3y/player/MusicApp.kt index 35ebfca..bb453f2 100644 --- a/src/main/kotlin/com/h0tk3y/player/MusicApp.kt +++ b/src/main/kotlin/com/h0tk3y/player/MusicApp.kt @@ -1,17 +1,32 @@ package com.h0tk3y.player -import java.io.File +import java.io.* +import java.net.URLClassLoader +import kotlin.reflect.KMutableProperty +import kotlin.reflect.full.* open class MusicApp( private val pluginClasspath: List, private val enabledPluginClasses: Set ) : AutoCloseable { + + private fun fileByPlugin(p: MusicPlugin) : File { + val f = File("pluginsData/" + p.pluginId + "Metadata.txt") + f.createNewFile() + return f + } + fun init() { + plugins.forEach { + FileInputStream(fileByPlugin(it)).use { input -> + it.init(input) + } + } /** - * TODO: Инициализировать плагины с помощью функции [MusicPlugin.init], - * предоставив им байтовые потоки их состояния (для тех плагинов, для которых они сохранены). - * Обратите внимание на cлучаи, когда необходимо выбрасывать исключения - * [IllegalPluginException] и [PluginClassNotFoundException]. + * Инициализировать плагины с помощью функции [MusicPlugin.init], + * предоставив им байтовые потоки их состояния (для тех плагинов, для которых они сохранены). + * Обратите внимание на cлучаи, когда необходимо выбрасывать исключения + * [IllegalPluginException] и [PluginClassNotFoundException]. **/ musicLibrary // access to initialize @@ -22,26 +37,62 @@ open class MusicApp( if (isClosed) return isClosed = true - /** TODO: Сохранить состояние плагинов с помощью [MusicPlugin.persist]. */ + plugins.forEach { + FileOutputStream(fileByPlugin(it)).use { output -> + it.persist(output) + } + } + /** Сохранить состояние плагинов с помощью [MusicPlugin.persist]. */ } fun wipePersistedPluginData() { - // TODO: Удалить сохранённое состояние плагинов. + plugins.forEach {fileByPlugin(it).delete() } + /** Удалить сохранённое состояние плагинов. */ } - private val pluginClassLoader: ClassLoader = TODO("Создать загрузчик классов для плагинов.") + private val pluginClassLoader: ClassLoader = URLClassLoader( + pluginClasspath.map { it.toURI().toURL() }.toTypedArray() + ) private val plugins: List by lazy { + enabledPluginClasses.map {plugin -> + val c = try { + pluginClassLoader.loadClass(plugin) + } catch (e: ClassNotFoundException) { + throw PluginClassNotFoundException(plugin) + } + val primaryCon = c.kotlin.primaryConstructor + if (primaryCon != null && primaryCon.parameters.size == 1 && + primaryCon.parameters[0].type.classifier == MusicApp::class) { + primaryCon.call(this) as MusicPlugin + } else { + val con = c.kotlin.constructors.singleOrNull { + it.parameters.isEmpty() + } ?: throw IllegalPluginException(c) + + val instance = con.call() as MusicPlugin + + val app = c.kotlin.memberProperties.find { + it is KMutableProperty<*> && it.name == "musicAppInstance" + && it.returnType.classifier == MusicApp::class + } ?: throw IllegalPluginException(c) + (app as KMutableProperty<*>).setter.call(instance, this) + + instance + } + } /** - * TODO используя [pluginClassLoader] и следуя контракту [MusicPlugin], - * загрузить плагины, перечисленные в [enabledPluginClasses]. - * Эта функция не должна вызывать [MusicPlugin.init] + * используя [pluginClassLoader] и следуя контракту [MusicPlugin], + * загрузить плагины, перечисленные в [enabledPluginClasses]. + * Эта функция не должна вызывать [MusicPlugin.init] */ - emptyList() } fun findSinglePlugin(pluginClassName: String): MusicPlugin? = - TODO("Если есть единственный плагин, принадлежащий типу по имени pluginClassName, вернуть его, иначе null.") + plugins.singleOrNull { + it::class.qualifiedName == pluginClassName + } + /** Если есть единственный плагин, принадлежащий fтипу по имени pluginClassName, вернуть его, иначе null.*/ fun getPlugins(pluginClass: Class): List = plugins.filterIsInstance(pluginClass) diff --git a/src/test/kotlin/com/h0tk3y/player/test/AdsPluginTest.kt b/src/test/kotlin/com/h0tk3y/player/test/AdsPluginTest.kt new file mode 100644 index 0000000..827b4a9 --- /dev/null +++ b/src/test/kotlin/com/h0tk3y/player/test/AdsPluginTest.kt @@ -0,0 +1,95 @@ +package com.h0tk3y.player.test + +import com.h0tk3y.player.* +import org.junit.Test +import java.io.File +import java.io.FileInputStream +import java.lang.IllegalArgumentException +import kotlin.reflect.full.memberProperties + +private val thirdPartyPluginClasses: List = + System.getProperty("third-party-plugin-classes").split(File.pathSeparator).map { File(it) } + +class AdsTests { + val track1 = Track(mutableMapOf()) { FileInputStream(File("sounds/beep-1.mp3")) } + val track2 = Track(mutableMapOf()) { FileInputStream(File("sounds/beep-2.mp3")) } + + private val defaultEnabledPlugins = setOf( + StaticPlaylistsLibraryContributor::class.java.canonicalName, + "com.h0tk3y.third.party.plugin.AdsPlugin" + ) + + private fun withApp(clear: Boolean = true, doTest: (TestableMusicApp) -> Unit) { + val app = TestableMusicApp(thirdPartyPluginClasses, defaultEnabledPlugins) + if (clear) { + app.wipePersistedPluginData() + } + app.use { + it.init() + doTest(it) + } + } + + private fun getAds(app: MusicApp) : List { + val p = app.findSinglePlugin("com.h0tk3y.third.party.plugin.AdsPlugin") ?: throw IllegalArgumentException() + val pr = p::class.memberProperties.find { it.name == "ads" } ?: throw IllegalArgumentException() + val res = pr.getter.call(p) ?: throw IllegalArgumentException() + return (res as List<*>).map { it as String } + } + + @Test + fun testAdsQueue() { + withApp { app -> + val playlist = Playlist("my", listOf(track1, track2, track1, track2, track1, track2)) + app.player.playbackState = PlaybackState.Playing(PlaylistPosition(playlist, 0), isResumed = false) + assert(getAds(app).isEmpty()) + app.player.finishedTrack() + app.player.finishedTrack() + assert(getAds(app).size == 1) + val firstAdd = getAds(app).first() + app.player.finishedTrack() + app.player.finishedTrack() + assert(getAds(app).size == 2) + assert(getAds(app).first() == firstAdd) + val lastAdd = getAds(app).last() + app.player.finishedTrack() + app.player.finishedTrack() + assert(getAds(app).size == 2) + assert(getAds(app).first() == lastAdd) + } + } + + @Test + fun testOneTrackPlaylist() { + withApp { app -> + val playlist = Playlist("my", listOf(track1, track1, track1, track1)) + app.player.playbackState = PlaybackState.Playing(PlaylistPosition(playlist, 0), isResumed = false) + app.wipePersistedPluginData() + assert(getAds(app).isEmpty()) + app.player.finishedTrack() + app.player.finishedTrack() + app.player.finishedTrack() + app.player.finishedTrack() + assert(getAds(app).size == 2) + } + } + + @Test + fun testPersistStates() { + var ads = mutableListOf() + withApp { app -> + val playlist = Playlist("my", listOf(track1, track2, track1, track2)) + app.player.playbackState = PlaybackState.Playing(PlaylistPosition(playlist, 0), isResumed = false) + assert(getAds(app).isEmpty()) + playlist.tracks.forEach { + app.player.finishedTrack() + } + ads = getAds(app).toMutableList() + app.close() + } + withApp(false) { app -> + val newAds = getAds(app).toMutableList() + assert(newAds == ads) + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/h0tk3y/player/test/TestableMusicApp.kt b/src/test/kotlin/com/h0tk3y/player/test/TestableMusicApp.kt index aaab635..e486ef6 100644 --- a/src/test/kotlin/com/h0tk3y/player/test/TestableMusicApp.kt +++ b/src/test/kotlin/com/h0tk3y/player/test/TestableMusicApp.kt @@ -4,6 +4,7 @@ import com.h0tk3y.player.* import org.junit.Before import org.junit.Test import java.io.File +import java.io.FileInputStream class TestableMusicApp( pluginClasspath: List, @@ -29,8 +30,12 @@ class AppTests { @Test fun testNone() { - val track1 = Track(mutableMapOf(), { TODO() }) - val track2 = Track(mutableMapOf(), { TODO() }) + val track1 = Track(mutableMapOf()) { + FileInputStream(File("sounds/beep-1.mp3")) + } + val track2 = Track(mutableMapOf()) { + FileInputStream(File("sounds/beep-2.mp3")) + } val playlist = Playlist("my", listOf(track1, track2)) app.player.playbackState = PlaybackState.Playing(PlaylistPosition(playlist, 0), isResumed = false) app.player.finishedTrack() diff --git a/third-party-plugin/build.gradle.kts b/third-party-plugin/build.gradle.kts index 4a77988..15e630d 100644 --- a/third-party-plugin/build.gradle.kts +++ b/third-party-plugin/build.gradle.kts @@ -8,5 +8,6 @@ repositories { dependencies { implementation(kotlin("stdlib")) + implementation(project(":")) compileOnly(rootProject) } \ No newline at end of file diff --git a/third-party-plugin/src/main/kotlin/com/h0tk3y/third/party/plugin/AdsPlugin.kt b/third-party-plugin/src/main/kotlin/com/h0tk3y/third/party/plugin/AdsPlugin.kt new file mode 100644 index 0000000..e8c3680 --- /dev/null +++ b/third-party-plugin/src/main/kotlin/com/h0tk3y/third/party/plugin/AdsPlugin.kt @@ -0,0 +1,60 @@ +package com.h0tk3y.third.party.plugin + +import com.h0tk3y.player.* +import java.io.FileInputStream +import java.io.InputStream +import java.io.OutputStream +import java.lang.IllegalArgumentException +import java.nio.channels.Channel +import java.util.* +import java.util.concurrent.ArrayBlockingQueue + +class AdsPlugin(override val musicAppInstance: MusicApp) : + PlaybackListenerPlugin { + + var ads = mutableListOf() + + var runCount: Int = 0 + private set + + companion object { + const val PERIOD = 2 + const val CAPACITY = 2 + val adTexts = listOf( + "Легендарный Бока и его внук популярный певец Жока", + "Не забывайте проветривать....", + "покупайте деньги!", + "Говорю мужу: ну и живот ты отрастил! Ответ убил..." + ) + } + + override fun onPlaybackStateChange(oldPlaybackState: PlaybackState, newPlaybackState: PlaybackState) { + if (oldPlaybackState !is PlaybackState.Paused && newPlaybackState !is PlaybackState.Paused) { + runCount++ + if (runCount >= PERIOD) { + runCount = 0 + if (ads.size == CAPACITY) { + ads.removeAt(0) + } + ads.add(adTexts[(0 until adTexts.size).random()]) + println(ads.last()) + } + } + } + + override fun init(persistedState: InputStream?) { + runCount = 0 + ads.clear() + persistedState?.reader()?.readLines()?.let {line -> + if (!line.isEmpty()) { + runCount = line[0].toInt() + line.subList(1, line.size).forEach { ads.add(it) } + } + } + } + + override fun persist(stateStream: OutputStream) { + val str = runCount.toString() + "\n" + ads.joinToString("\n") + stateStream.write(str.toByteArray()) + } +}