mirror of
https://github.com/KDE/kdeconnect-android
synced 2025-08-22 09:58:08 +00:00
Migrate video urls helper to Kotlin
- Migrated code to Kotlin - Fixed crash - Added unit tests
This commit is contained in:
parent
35e8ea0c4c
commit
c9fb81363d
@ -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));
|
||||
}
|
||||
}
|
80
src/org/kde/kdeconnect/Helpers/VideoUrlsHelper.kt
Normal file
80
src/org/kde/kdeconnect/Helpers/VideoUrlsHelper.kt
Normal 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}"
|
||||
}
|
||||
}
|
75
tests/org/kde/kdeconnect/Helpers/VideoUrlsHelperTest.kt
Normal file
75
tests/org/kde/kdeconnect/Helpers/VideoUrlsHelperTest.kt
Normal 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())
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user