2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-08-22 18:07:55 +00:00

Migrate video urls helper to Kotlin

- Migrated code to Kotlin
- Fixed crash
- Added unit tests
This commit is contained in:
TPJ Schikhof 2024-08-18 21:54:42 +00:00 committed by Albert Vaca Cintora
parent 35e8ea0c4c
commit c9fb81363d
3 changed files with 155 additions and 151 deletions

View File

@ -1,151 +0,0 @@
/*
* SPDX-FileCopyrightText: 2020 Soul Trace <S-trace@list.ru>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Helpers;
import org.apache.commons.lang3.StringUtils;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Locale;
public class VideoUrlsHelper {
private static final int SECONDS_IN_MINUTE = 60;
private static final int MINUTES_IN_HOUR = 60;
private static final int SECONDS_IN_HOUR = SECONDS_IN_MINUTE * MINUTES_IN_HOUR;
public static URL formatUriWithSeek(String address, long position)
throws MalformedURLException {
URL url = new URL(address);
position /= 1000; // Convert ms to seconds
if (position <= 0) {
return url; // nothing to do
}
String host = url.getHost().toLowerCase();
// Most common settings as defaults:
String parameter = "t="; // Characters before timestamp
String timestamp = Long.toString(position); // Timestamp itself
String trailer = ""; // Characters after timestamp
// true - search/add to query URL part (between ? and # signs),
// false - search/add timestamp to ref (anchor) URL part (after # sign),
boolean inQuery = true;
// true - We know how to format URL with seek timestamp, false - not
boolean seekUrl = false;
// Override defaults if necessary
if (StringUtils.containsAny(host, "youtube.com", "youtu.be", "pornhub.com")) {
seekUrl = true;
url = stripTimestampS(url, parameter, trailer, inQuery);
} else if (host.contains("vimeo.com")) {
seekUrl = true;
trailer = "s";
url = stripTimestampS(url, parameter, trailer, inQuery);
} else if (host.contains("dailymotion.com")) {
seekUrl = true;
parameter = "start=";
url = stripTimestampS(url, parameter, trailer, inQuery);
} else if (host.contains("twitch.tv")) {
seekUrl = true;
timestamp = formatTimestampHMS(position, true);
url = stripTimestampHMS(url, parameter, trailer, inQuery);
}
if (seekUrl) {
url = formatUrlWithSeek(url, timestamp, parameter, trailer, inQuery);
}
return url;
}
// Returns timestamp in 1h2m34s or 01h02m34s (according to padWithZeroes)
private static String formatTimestampHMS(long seconds, boolean padWithZeroes) {
if (seconds == 0) {
return "0s";
}
int sec = (int) (seconds % SECONDS_IN_MINUTE);
int min = (int) ((seconds / SECONDS_IN_MINUTE) % MINUTES_IN_HOUR);
int hour = (int) (seconds / SECONDS_IN_HOUR);
String hours = hour > 0 ? hour + "h" : "";
String mins = min > 0 || hour > 0 ? min + "m" : "";
String secs = sec + "s";
String value;
if (padWithZeroes) {
String hoursPad = hour > 9 ? "" : "0";
String minsPad = min > 9 ? "" : "0";
String secsPad = sec > 9 ? "" : "0";
value = hoursPad + hours + minsPad + mins + secsPad + secs;
} else {
value = hours + mins + secs;
}
return value;
}
// Remove timestamp in 01h02m34s or 1h2m34s or 02m34s or 2m34s or 01s or 1s format.
// Can also nandle rimestamps in 1234s format if called with 's' trailer
private static URL stripTimestampHMS(URL url, String parameter, String trailer, boolean inQuery)
throws MalformedURLException {
String regex = parameter + "([\\d]+[hH])?([\\d]+[mM])?[\\d]+[sS]" + trailer + "&?";
return stripTimestampCommon(url, inQuery, regex);
}
// Remove timestamp in 1234 format
private static URL stripTimestampS(URL url, String parameter, String trailer, boolean inQuery)
throws MalformedURLException {
String regex = parameter + "[\\d]+" + trailer + "&?";
return stripTimestampCommon(url, inQuery, regex);
}
private static URL stripTimestampCommon(URL url, boolean inQuery, String regex)
throws MalformedURLException {
String value;
if (inQuery) {
value = url.getQuery();
} else {
value = url.getRef();
}
if (value == null) {
return url;
}
String newValue = value.replaceAll(regex, "");
String replaced = url.toString().replaceFirst(value, newValue);
if (inQuery && replaced.endsWith("&")) {
replaced = replaced.substring(0, replaced.length() - 1);
}
return new URL(replaced);
}
private static URL formatUrlWithSeek(URL url, String position, String parameter, String trailer,
boolean inQuery) throws MalformedURLException {
String value;
String separator;
String newValue;
if (inQuery) {
value = url.getQuery();
separator = "?";
} else {
value = url.getRef();
separator = "#";
}
if (value == null) {
newValue = String.format(Locale.getDefault(), "%s%s%s%s%s",
url, separator, parameter, position, trailer);
return new URL(newValue);
}
if (inQuery) {
newValue = String.format(Locale.getDefault(), "%s&%s%s%s",
value, parameter, position, trailer);
} else {
newValue = String.format(Locale.getDefault(), "%s%s%s",
parameter, position, trailer);
}
return new URL(url.toString().replaceFirst(value, newValue));
}
}

View File

@ -0,0 +1,80 @@
/*
* SPDX-FileCopyrightText: 2024 TPJ Schikhof <kde@schikhof.eu>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Helpers
import java.net.MalformedURLException
import java.net.URL
object VideoUrlsHelper {
private const val SECONDS_IN_MINUTE = 60
private const val MINUTES_IN_HOUR = 60
private const val SECONDS_IN_HOUR = SECONDS_IN_MINUTE * MINUTES_IN_HOUR
@Throws(MalformedURLException::class)
fun formatUriWithSeek(address: String, position: Long): URL {
val positionSeconds = position / 1000 // milliseconds to seconds
val url = URL(address)
if (positionSeconds <= 0) {
return url // nothing to do
}
val host = url.host.lowercase()
return when {
listOf("youtube.com", "youtu.be", "pornhub.com").any { site -> site in host } -> {
url.editParameter("t", Regex("\\d+")) { "$positionSeconds" }
}
host.contains("vimeo.com") -> {
url.editParameter("t", Regex("\\d+s")) { "${positionSeconds}s" }
}
host.contains("dailymotion.com") -> {
url.editParameter("start", Regex("\\d+")) { "$positionSeconds" }
}
host.contains("twitch.tv") -> {
url.editParameter("t", Regex("(\\d+[hH])?(\\d+[mM])?\\d+[sS]")) { positionSeconds.formatTimestampHMS() }
}
else -> url
}
}
private fun URL.editParameter(parameter: CharSequence, valuePattern: Regex?, parameterValueModifier: (String) -> String): URL {
// "https://www.youtube.com/watch?v=ovX5G0O5ZvA&t=13" -> ["https://www.youtube.com/watch", "v=ovX5G0O5ZvA&t=13"]
val urlSplit = this.toString().split("?")
if (urlSplit.size != 2) {
return this
}
val (urlBase, urlQuery) = urlSplit
val modifiedUrlQuery = urlQuery
.split("&") // "v=ovX5G0O5ZvA&t=13" -> ["v=ovX5G0O5ZvA", "t=13"]
.map { it.split("=", limit = 2) } // […, "t=13"] -> […, ["t", "13"]]
.map { Pair(it.first(), it.lastOrNull() ?: return this) }
.map { paramAndValue ->
// Modify matching parameter and optionally matches the old value with the provided pattern
if (paramAndValue.first == parameter && valuePattern?.matches(paramAndValue.second) != false) {
Pair(paramAndValue.first, parameterValueModifier(paramAndValue.second)) // ["t", "13"] -> ["t", result]
} else {
paramAndValue
}
}
.joinToString("&") { "${it.first}=${it.second}" } // [["v", "ovX5G0O5ZvA"], ["t", "14"]] -> "v=ovX5G0O5ZvA&t=14"
return URL("${urlBase}?${modifiedUrlQuery}") // -> "https://www.youtube.com/watch?v=ovX5G0O5ZvA&t=14"
}
/** Formats timestamp to e.g. 01h02m34s */
private fun Long.formatTimestampHMS(): String {
if (this == 0L) return "0s"
val seconds: Long = this % SECONDS_IN_MINUTE
val minutes: Long = (this / SECONDS_IN_MINUTE) % MINUTES_IN_HOUR
val hours: Long = this / SECONDS_IN_HOUR
fun pad(s: String) = s.padStart(3, '0')
val hoursText = if (hours > 0) pad("${hours}h") else ""
val minutesText = if (minutes > 0 || hours > 0) pad("${minutes}m") else ""
val secondsText = pad("${seconds}s")
return "${hoursText}${minutesText}${secondsText}"
}
}

View File

@ -0,0 +1,75 @@
/*
* SPDX-FileCopyrightText: 2024 TPJ Schikhof <kde@schikhof.eu>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Helpers
import org.junit.Assert
import org.junit.Test
class VideoUrlsHelperTest {
@Test
fun checkYoutubeURL() {
val url = "https://www.youtube.com/watch?v=ovX5G0O5ZvA&t=13"
val formatted = VideoUrlsHelper.formatUriWithSeek(url, 51_000L)
val expected = "https://www.youtube.com/watch?v=ovX5G0O5ZvA&t=51"
Assert.assertEquals(expected, formatted.toString())
}
@Test
fun checkYoutubeURLSubSecond() {
val url = "https://www.youtube.com/watch?v=ovX5G0O5ZvA&t=13"
val formatted = VideoUrlsHelper.formatUriWithSeek(url, 450L)
val expected = "https://www.youtube.com/watch?v=ovX5G0O5ZvA&t=13"
Assert.assertEquals(expected, formatted.toString())
}
@Test
fun checkVimeoURL() {
val url = "https://vimeo.com/347119375?foo=bar&t=13s"
val formatted = VideoUrlsHelper.formatUriWithSeek(url, 51_000L)
val expected = "https://vimeo.com/347119375?foo=bar&t=51s"
Assert.assertEquals(expected, formatted.toString())
}
@Test
fun checkVimeoURLSubSecond() {
val url = "https://vimeo.com/347119375?foo=bar&t=13s"
val formatted = VideoUrlsHelper.formatUriWithSeek(url, 450L)
val expected = "https://vimeo.com/347119375?foo=bar&t=13s"
Assert.assertEquals(expected, formatted.toString())
}
@Test
fun checkVimeoURLParamOrderCrash() {
val url = "https://vimeo.com/347119375?t=13s"
val formatted = VideoUrlsHelper.formatUriWithSeek(url, 51_000L)
val expected = "https://vimeo.com/347119375?t=51s"
Assert.assertEquals(expected, formatted.toString())
}
@Test
fun checkDailymotionURL() {
val url = "https://www.dailymotion.com/video/xnopyt?foo=bar&start=13"
val formatted = VideoUrlsHelper.formatUriWithSeek(url, 51_000L)
val expected = "https://www.dailymotion.com/video/xnopyt?foo=bar&start=51"
Assert.assertEquals(expected, formatted.toString())
}
@Test
fun checkTwitchURL() {
val url = "https://www.twitch.tv/videos/123?foo=bar&t=1h2m3s"
val formatted = VideoUrlsHelper.formatUriWithSeek(url, 10_000_000)
val expected = "https://www.twitch.tv/videos/123?foo=bar&t=02h46m40s"
Assert.assertEquals(expected, formatted.toString())
}
@Test
fun checkUnknownURL() {
val url = "https://example.org/cool_video.mp4"
val formatted = VideoUrlsHelper.formatUriWithSeek(url, 51_000L)
val expected = "https://example.org/cool_video.mp4"
Assert.assertEquals(expected, formatted.toString())
}
}