-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
332 additions
and
585 deletions.
There are no files selected for viewing
95 changes: 95 additions & 0 deletions
95
common/src/androidMain/kotlin/me/konyaco/collinsdictionary/service/AndroidSoundPlayer.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
package me.konyaco.collinsdictionary.service | ||
|
||
import android.content.Context | ||
import android.media.AudioAttributes | ||
import android.media.MediaPlayer | ||
import android.util.Log | ||
import androidx.core.net.toUri | ||
import io.ktor.client.* | ||
import io.ktor.client.request.* | ||
import io.ktor.client.statement.* | ||
import kotlinx.coroutines.* | ||
import java.io.File | ||
import kotlin.coroutines.resume | ||
|
||
private const val TAG = "AndroidSoundPlayer.android" | ||
|
||
class AndroidSoundPlayer : SoundPlayer { | ||
var context: Context? = null | ||
|
||
override fun play( | ||
url: String, | ||
onStart: () -> Unit, | ||
onStop: () -> Unit, | ||
onError: (e: Throwable) -> Unit | ||
) { | ||
job.let { | ||
job = scope.async(Dispatchers.IO) { | ||
val file = try { | ||
getFile(url) | ||
} catch (e: Throwable) { | ||
Log.w(TAG, "Failed to download sound", e) | ||
onError(e) | ||
return@async | ||
} | ||
it?.cancelAndJoin() | ||
try { | ||
onStart() | ||
playMedia(file) | ||
onStop() | ||
} catch (e: Throwable) { | ||
Log.w("Failed to play sound", e) | ||
onError(e) | ||
} | ||
} | ||
} | ||
} | ||
|
||
private val scope = CoroutineScope(Dispatchers.Default) | ||
private var job: Job? = null | ||
|
||
private suspend fun playMedia(file: File) { | ||
suspendCancellableCoroutine<Unit> { continuation -> | ||
val mediaPlayer = MediaPlayer() | ||
continuation.invokeOnCancellation { | ||
mediaPlayer.pause() | ||
mediaPlayer.release() | ||
} | ||
with(mediaPlayer) { | ||
setOnCompletionListener { | ||
release() | ||
continuation.resume(Unit) | ||
} | ||
setAudioAttributes( | ||
AudioAttributes.Builder() | ||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) | ||
.setUsage(AudioAttributes.USAGE_MEDIA) | ||
.build() | ||
) | ||
setDataSource(context!!, file.toUri()) | ||
prepare() | ||
start() | ||
} | ||
} | ||
} | ||
|
||
private fun directory(): File = | ||
context!!.externalCacheDir!!.resolve("sound").also { it.mkdir() } | ||
|
||
// Get filename part of url (xx://xx/xxx.mp3) | ||
private fun fileNameFromUrl(url: String) = url.substringAfterLast("/") | ||
|
||
private suspend fun getFile(url: String): File { | ||
val fileName = fileNameFromUrl(url) | ||
val file = directory().resolve(fileName) | ||
if (!file.exists()) { | ||
val bytes = HttpClient().get(url).readBytes() | ||
withContext(Dispatchers.IO) { | ||
file.outputStream().use { | ||
it.write(bytes) | ||
} | ||
} | ||
} | ||
return file | ||
} | ||
} |
95 changes: 1 addition & 94 deletions
95
common/src/androidMain/kotlin/me/konyaco/collinsdictionary/service/SoundPlayer.android.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 20 additions & 1 deletion
21
common/src/commonMain/kotlin/me/konyaco/collinsdictionary/service/CollinsOnlineDictionary.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,22 @@ | ||
package me.konyaco.collinsdictionary.service | ||
|
||
expect class CollinsOnlineDictionary(): CollinsDictionary | ||
import me.konyaco.collinsdictionary.service.internal.JsoupCollinsParser | ||
import me.konyaco.collinsdictionary.service.internal.CollinsHttpRequester | ||
|
||
class CollinsOnlineDictionary : CollinsDictionary { | ||
private val parser = JsoupCollinsParser() | ||
private val requester = CollinsHttpRequester() | ||
|
||
override suspend fun search(word: String): SearchResult { | ||
return when (val result = requester.search(word)) { | ||
is CollinsHttpRequester.SearchResult.PreciseWord -> SearchResult.PreciseWord(word) | ||
is CollinsHttpRequester.SearchResult.Redirect -> SearchResult.Redirect(result.redirectWord) | ||
is CollinsHttpRequester.SearchResult.NotFound -> SearchResult.NotFound(parser.parseAlternatives(result.wordListHtml)) | ||
} | ||
} | ||
|
||
override suspend fun getDefinition(word: String): Word? { | ||
val html = requester.getDefinition(word) | ||
return parser.parseContent(html) | ||
} | ||
} |
92 changes: 92 additions & 0 deletions
92
...c/commonMain/kotlin/me/konyaco/collinsdictionary/service/internal/CollinsHttpRequester.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
package me.konyaco.collinsdictionary.service.internal | ||
|
||
import io.ktor.client.* | ||
import io.ktor.client.engine.* | ||
import io.ktor.client.engine.cio.* | ||
import io.ktor.client.plugins.* | ||
import io.ktor.client.request.* | ||
import io.ktor.client.statement.* | ||
import io.ktor.http.* | ||
import java.net.ProxySelector | ||
import java.net.URI | ||
|
||
internal class CollinsHttpRequester { | ||
private val client = HttpClient(CIO) { | ||
setupClient() | ||
followRedirects = false | ||
} | ||
private val clientFollowRedirect = HttpClient(CIO) { | ||
setupClient() | ||
} | ||
|
||
private fun HttpClientConfig<*>.setupClient() { | ||
engine { | ||
val proxy = | ||
ProxySelector.getDefault().select(URI("https://www.collinsdictionary.com")) | ||
.firstOrNull() | ||
if (proxy != null && proxy.type != ProxyType.UNKNOWN) { | ||
this.proxy = proxy | ||
} | ||
} | ||
} | ||
|
||
companion object { | ||
private const val SEARCH_URL = "https://www.collinsdictionary.com/search" | ||
private const val SPELL_CHECK_URL = "https://www.collinsdictionary.com/spellcheck/english" | ||
private const val DICTIONARY_URL = "https://www.collinsdictionary.com/dictionary/english" | ||
private fun buildSearchURL(word: String) = "$SEARCH_URL/?dictCode=english&q=$word" | ||
private fun buildDictionaryURL(word: String) = "$DICTIONARY_URL/$word" | ||
} | ||
|
||
suspend fun getDefinition(word: String): String { | ||
return clientFollowRedirect.get(buildDictionaryURL(word)).bodyAsText() | ||
} | ||
|
||
sealed interface SearchResult { | ||
data class PreciseWord(val contentHtml: String): SearchResult | ||
data class Redirect(val redirectWord: String): SearchResult | ||
data class NotFound(val wordListHtml: String): SearchResult | ||
} | ||
|
||
suspend fun search(word: String): SearchResult { | ||
val response = try { | ||
client.get(buildSearchURL(word)) | ||
} catch (e: RedirectResponseException) { | ||
e.response | ||
} | ||
|
||
if (response.status == HttpStatusCode.Found) { | ||
val redirectedUrl = | ||
response.headers[HttpHeaders.Location] ?: error("Redirect to header was not found.") | ||
|
||
return if (isPrecise(redirectedUrl)) { | ||
val redirectWord = getRedirectedWord(redirectedUrl) | ||
if (redirectWord == word) { | ||
SearchResult.PreciseWord(redirectWord) | ||
} else { | ||
SearchResult.Redirect(redirectWord) | ||
} | ||
} else { | ||
val html = client.get(redirectedUrl).bodyAsText() // Get response in [spellcheck] | ||
SearchResult.NotFound(html) | ||
} | ||
} else { | ||
error("Response code is: ${response.status}") | ||
} | ||
} | ||
|
||
/** | ||
* If the search result redirects to a precise page. Or to a spellcheck page. | ||
*/ | ||
private fun isPrecise(redirectedUrl: String): Boolean { | ||
return when { | ||
redirectedUrl.startsWith(DICTIONARY_URL) -> true | ||
redirectedUrl.startsWith(SPELL_CHECK_URL) -> false | ||
else -> error("Could not search word.") | ||
} | ||
} | ||
|
||
private fun getRedirectedWord(redirectedUrl: String): String = | ||
redirectedUrl.substringAfterLast("/") | ||
|
||
} |
Oops, something went wrong.