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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/main/kotlin/com/h0tk3y/player/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ fun main(args: Array<String>) {
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()
}
77 changes: 64 additions & 13 deletions src/main/kotlin/com/h0tk3y/player/MusicApp.kt
Original file line number Diff line number Diff line change
@@ -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<File>,
private val enabledPluginClasses: Set<String>
) : 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
Expand All @@ -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<MusicPlugin> 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<MusicPlugin>()
}

fun findSinglePlugin(pluginClassName: String): MusicPlugin? =
TODO("Если есть единственный плагин, принадлежащий типу по имени pluginClassName, вернуть его, иначе null.")
plugins.singleOrNull {
it::class.qualifiedName == pluginClassName
}
/** Если есть единственный плагин, принадлежащий fтипу по имени pluginClassName, вернуть его, иначе null.*/

fun <T : MusicPlugin> getPlugins(pluginClass: Class<T>): List<T> =
plugins.filterIsInstance(pluginClass)
Expand Down
95 changes: 95 additions & 0 deletions src/test/kotlin/com/h0tk3y/player/test/AdsPluginTest.kt
Original file line number Diff line number Diff line change
@@ -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<File> =
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<String> {
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<String>()
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)
}
}
}
9 changes: 7 additions & 2 deletions src/test/kotlin/com/h0tk3y/player/test/TestableMusicApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<File>,
Expand All @@ -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()
Expand Down
1 change: 1 addition & 0 deletions third-party-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ repositories {

dependencies {
implementation(kotlin("stdlib"))
implementation(project(":"))
compileOnly(rootProject)
}
Original file line number Diff line number Diff line change
@@ -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<String>()

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())
}
}