Skip to content
This repository has been archived by the owner on Dec 6, 2024. It is now read-only.

Commit

Permalink
internally read playlist files via "Open with" (local & web)
Browse files Browse the repository at this point in the history
  • Loading branch information
y20k authored and jamal2362 committed Sep 15, 2023
1 parent 392afd5 commit db70a73
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 148 deletions.
28 changes: 21 additions & 7 deletions app/src/main/java/com/jamal2367/urlradio/PlayerFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.PickVisualMediaRequest
Expand Down Expand Up @@ -573,13 +574,26 @@ class PlayerFragment : Fragment(),

/* Handles ACTION_VIEW request to add Station */
private fun handleViewIntent() {
val contentUri: Uri? = (activity as Activity).intent.data
if (contentUri != null) {
val scheme: String = contentUri.scheme ?: String()
if (scheme.startsWith("http")) DownloadHelper.downloadPlaylists(
activity as Context,
arrayOf(contentUri.toString())
)
val intentUri: Uri? = (activity as Activity).intent.data
if (intentUri != null) {
CoroutineScope(IO).launch {
val stationList: MutableList<Station> = mutableListOf()
val scheme: String = intentUri.scheme ?: String()
if (scheme.startsWith("http")) {
Log.i(TAG, "URL Radio was started to handle a web link.")
stationList.addAll(CollectionHelper.createStationsFromUrl(intentUri.toString()))
} else if (scheme.startsWith("content")) {
Log.i(TAG, "URL Radio was started to handle a local audio playlist.")
stationList.addAll(CollectionHelper.createStationListFromContentUri(activity as Context, intentUri))
}
if (stationList.isNotEmpty()) {
// todo hand over station list to a new AddStationDialog
Log.e(TAG, stationList.toString()) // todo remove
} else {
// invalid address
Toast.makeText(context, R.string.toastmessage_station_not_valid, Toast.LENGTH_LONG).show()
}
}
}
}

Expand Down
130 changes: 130 additions & 0 deletions app/src/main/java/com/jamal2367/urlradio/helpers/CollectionHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import com.jamal2367.urlradio.Keys
import com.jamal2367.urlradio.R
import com.jamal2367.urlradio.core.Collection
import com.jamal2367.urlradio.core.Station
import com.jamal2367.urlradio.search.DirectInputCheck
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
import java.io.File
Expand Down Expand Up @@ -427,6 +428,135 @@ object CollectionHelper {
}


/* Creates station from playlist URLs and stream address URLs */
suspend fun createStationsFromUrl(query: String, lastCheckedAddress: String = String()): List<Station> {
val stationList: MutableList<Station> = mutableListOf()
val contentType: String = NetworkHelper.detectContentType(query).type.lowercase(Locale.getDefault())
val directInputCheck: DirectInputCheck? = null

// CASE: M3U playlist detected
if (Keys.MIME_TYPES_M3U.contains(contentType)) {
val lines: List<String> = NetworkHelper.downloadPlaylist(query)
stationList.addAll(readM3uPlaylistContent(lines))
}
// CASE: PLS playlist detected
else if (Keys.MIME_TYPES_PLS.contains(contentType)) {
val lines: List<String> = NetworkHelper.downloadPlaylist(query)
stationList.addAll(readPlsPlaylistContent(lines))
}
// CASE: stream address detected
else if (Keys.MIME_TYPES_MPEG.contains(contentType) or
Keys.MIME_TYPES_OGG.contains(contentType) or
Keys.MIME_TYPES_AAC.contains(contentType) or
Keys.MIME_TYPES_HLS.contains(contentType)) {
// process Icecast stream and extract metadata
directInputCheck?.processIcecastStream(query, stationList)
// create station and add to collection
val station = Station(name = query, streamUris = mutableListOf(query), streamContent = contentType, modificationDate = GregorianCalendar.getInstance().time)
if (lastCheckedAddress != query) {
stationList.add(station)
}
}
return stationList
}


/* Creates station from URI pointing to a local file */
fun createStationListFromContentUri(context: Context, contentUri: Uri): List<Station> {
val stationList: MutableList<Station> = mutableListOf()
val fileType: String = FileHelper.getContentType(context, contentUri)
// CASE: M3U playlist detected
if (Keys.MIME_TYPES_M3U.contains(fileType)) {
val playlist = FileHelper.readTextFileFromContentUri(context, contentUri)
stationList.addAll(readM3uPlaylistContent(playlist))
}
// CASE: PLS playlist detected
else if (Keys.MIME_TYPES_PLS.contains(fileType)) {
val playlist = FileHelper.readTextFileFromContentUri(context, contentUri)
stationList.addAll(readPlsPlaylistContent(playlist))
}
return stationList
}


/* Reads a m3u playlist and returns a list of stations */
private fun readM3uPlaylistContent(playlist: List<String>): List<Station> {
val stations: MutableList<Station> = mutableListOf()
var name = String()
var streamUri: String
var contentType: String

playlist.forEach { line ->
// get name of station
if (line.startsWith("#EXTINF:")) {
name = line.substringAfter(",").trim()
}
// get stream uri and check mime type
else if (line.isNotBlank() && !line.startsWith("#")) {
streamUri = line.trim()
// use the stream address as the name if no name is specified
if (name.isEmpty()) {
name = streamUri
}
contentType = NetworkHelper.detectContentType(streamUri).type.lowercase(Locale.getDefault())
// store station in list if mime type is supported
if (contentType != Keys.MIME_TYPE_UNSUPPORTED) {
val station = Station(name = name, streamUris = mutableListOf(streamUri), streamContent = contentType, modificationDate = GregorianCalendar.getInstance().time)
stations.add(station)
}
// reset name for the next station - useful if playlist does not provide name(s)
name = String()
}
}
return stations
}


/* Reads a pls playlist and returns a list of stations */
private fun readPlsPlaylistContent(playlist: List<String>): List<Station> {
val stations: MutableList<Station> = mutableListOf()
var name = String()
var streamUri: String
var contentType: String

playlist.forEachIndexed { index, line ->
// get stream uri and check mime type
if (line.startsWith("File")) {
streamUri = line.substringAfter("=").trim()
contentType = NetworkHelper.detectContentType(streamUri).type.lowercase(Locale.getDefault())
if (contentType != Keys.MIME_TYPE_UNSUPPORTED) {
// look for the matching station name
val number: String = line.substring(4 /* File */, line.indexOf("="))
val lineBeforeIndex: Int = index - 1
val lineAfterIndex: Int = index + 1
// first: check the line before
if (lineBeforeIndex >= 0) {
val lineBefore: String = playlist[lineBeforeIndex]
if (lineBefore.startsWith("Title$number")) {
name = lineBefore.substringAfter("=").trim()
}
}
// then: check the line after
if (name.isEmpty() && lineAfterIndex < playlist.size) {
val lineAfter: String = playlist[lineAfterIndex]
if (lineAfter.startsWith("Title$number")) {
name = lineAfter.substringAfter("=").trim()
}
}
// fallback: use stream uri as name
if (name.isEmpty()) {
name = streamUri
}
// add station
val station = Station(name = name, streamUris = mutableListOf(streamUri), streamContent = contentType, modificationDate = GregorianCalendar.getInstance().time)
stations.add(station)
}
}
}
return stations
}


/* Export collection of stations as M3U */
fun exportCollectionM3u(context: Context, collection: Collection) {
Log.v(TAG, "Exporting collection of stations as M3U")
Expand Down
29 changes: 26 additions & 3 deletions app/src/main/java/com/jamal2367/urlradio/helpers/FileHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ object FileHelper {
fun readCollection(context: Context): Collection {
Log.v(TAG, "Reading collection - Thread: ${Thread.currentThread().name}")
// get JSON from text file
val json: String = readTextFile(context)
val json: String = readTextFileFromFile(context)
var collection = Collection()
if (json.isNotBlank()) {
// convert JSON and return as collection
Expand Down Expand Up @@ -416,7 +416,7 @@ object FileHelper {


/* Reads InputStream from file uri and returns it as String */
private fun readTextFile(context: Context): String {
private fun readTextFileFromFile(context: Context): String {
// todo read https://commonsware.com/blog/2016/03/15/how-consume-content-uri.html
// https://developer.android.com/training/secure-file-sharing/retrieve-info

Expand All @@ -425,7 +425,7 @@ object FileHelper {
if (!file.exists() || !file.canRead()) {
return String()
}
// readSuspended until last line reached
// read until last line reached
val stream: InputStream = file.inputStream()
val reader = BufferedReader(InputStreamReader(stream))
val builder: StringBuilder = StringBuilder()
Expand All @@ -438,6 +438,29 @@ object FileHelper {
}


/* Reads InputStream from content uri and returns it as List of String */
fun readTextFileFromContentUri(context: Context, contentUri: Uri): List<String> {
val lines: MutableList<String> = mutableListOf()
try {
// open input stream from content URI
val inputStream: InputStream? = context.contentResolver.openInputStream(contentUri)
if (inputStream != null) {
val reader: InputStreamReader = inputStream.reader()
var index = 0
reader.forEachLine {
index += 1
if (index < 256)
lines.add(it)
}
inputStream.close()
}
} catch (e: Exception) {
e.printStackTrace()
}
return lines
}


/* Writes given text to file on storage */
@Suppress("SameParameterValue")
private fun writeTextFile(context: Context, text: String, folder: String, fileName: String) {
Expand Down
15 changes: 15 additions & 0 deletions app/src/main/java/com/jamal2367/urlradio/helpers/NetworkHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,21 @@ object NetworkHelper {
}


/* Download playlist - up to 100 lines, with max. 200 characters */
fun downloadPlaylist(playlistUrlString: String): List<String> {
val lines = mutableListOf<String>()
val connection = URL(playlistUrlString).openConnection()
val reader = connection.getInputStream().bufferedReader()
reader.useLines { sequence ->
sequence.take(100).forEach { line ->
val trimmedLine = line.take(2000)
lines.add(trimmedLine)
}
}
return lines
}


/* Suspend function: Detects content type (mime type) from given URL string - async using coroutine */
suspend fun detectContentTypeSuspended(urlString: String): ContentType {
return suspendCoroutine { cont ->
Expand Down
Loading

0 comments on commit db70a73

Please sign in to comment.