diff --git a/README.md b/README.md index f78725338..987327ab8 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@


ScreenshotsDescriptionFeaturesUpdatesContributionDonateLicense

-

WebsiteBlogPress

+

WebsiteBlogFAQPress


WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY. diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 853bb6d68..44966744b 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -832,7 +832,6 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck psArgs = new String[]{ selectedStream.getFormat().getSuffix(), "false",// ignore empty frames - "false",// detect youtube duplicate lines }; } break; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 14e989625..f59cfaef0 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -79,6 +79,7 @@ import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.InfoCache; +import org.schabi.newpipe.util.KoreUtil; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; @@ -624,7 +625,7 @@ public class VideoDetailFragment url.replace("https", "http"))); } catch (Exception e) { if (DEBUG) Log.i(TAG, "Failed to start kore", e); - showInstallKoreDialog(activity); + KoreUtil.showInstallKoreDialog(activity); } return true; default: @@ -632,16 +633,6 @@ public class VideoDetailFragment } } - private static void showInstallKoreDialog(final Context context) { - final AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setMessage(R.string.kore_not_found) - .setPositiveButton(R.string.install, (DialogInterface dialog, int which) -> - NavigationHelper.installKore(context)) - .setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> { - }); - builder.create().show(); - } - private void setupActionBarOnError(final String url) { if (DEBUG) Log.d(TAG, "setupActionBarHandlerOnError() called with: url = [" + url + "]"); Log.e("-----", "missing code"); diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java index 761b50d85..1b5b5d07c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java @@ -55,10 +55,7 @@ public final class BackgroundPlayerActivity extends ServicePlayerActivity { return true; } - this.player.setRecovery(); - getApplicationContext().sendBroadcast(getPlayerShutdownIntent()); - getApplicationContext().startService(getSwitchIntent(PopupVideoPlayer.class)); - return true; + return switchTo(PopupVideoPlayer.class); } return false; } diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 6452a9850..46ca3921d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -150,6 +150,8 @@ public abstract class BasePlayer implements @NonNull public static final String RESUME_PLAYBACK = "resume_playback"; @NonNull + public static final String START_PAUSED = "start_paused"; + @NonNull public static final String SELECT_ON_APPEND = "select_on_append"; /*////////////////////////////////////////////////////////////////////////// @@ -304,7 +306,7 @@ public abstract class BasePlayer implements } // Good to go... initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, - /*playOnInit=*/true); + /*playOnInit=*/!intent.getBooleanExtra(START_PAUSED, false)); } protected void initPlayback(@NonNull final PlayQueue queue, @@ -944,10 +946,10 @@ public abstract class BasePlayer implements public void onPlayPause() { if (DEBUG) Log.d(TAG, "onPlayPause() called"); - if (!isPlaying()) { - onPlay(); - } else { + if (isPlaying()) { onPause(); + } else { + onPlay(); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index 284d10b42..487629c12 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -28,6 +28,7 @@ import android.database.ContentObserver; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.media.AudioManager; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; @@ -75,6 +76,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback; import org.schabi.newpipe.player.resolver.MediaSourceTag; import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; import org.schabi.newpipe.util.AnimationUtils; +import org.schabi.newpipe.util.KoreUtil; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; @@ -440,6 +442,7 @@ public final class MainVideoPlayer extends AppCompatActivity private boolean queueVisible; private ImageButton moreOptionsButton; + private ImageButton kodiButton; private ImageButton shareButton; private ImageButton toggleOrientationButton; private ImageButton switchPopupButton; @@ -476,6 +479,7 @@ public final class MainVideoPlayer extends AppCompatActivity this.moreOptionsButton = rootView.findViewById(R.id.moreOptionsButton); this.secondaryControls = rootView.findViewById(R.id.secondaryControls); + this.kodiButton = rootView.findViewById(R.id.kodi); this.shareButton = rootView.findViewById(R.id.share); this.toggleOrientationButton = rootView.findViewById(R.id.toggleOrientation); this.switchBackgroundButton = rootView.findViewById(R.id.switchBackground); @@ -487,6 +491,9 @@ public final class MainVideoPlayer extends AppCompatActivity titleTextView.setSelected(true); channelTextView.setSelected(true); + boolean showKodiButton = PreferenceManager.getDefaultSharedPreferences(this.context).getBoolean( + this.context.getString(R.string.show_play_with_kodi_key), false); + kodiButton.setVisibility(showKodiButton ? View.VISIBLE : View.GONE); getRootView().setKeepScreenOn(true); } @@ -523,6 +530,7 @@ public final class MainVideoPlayer extends AppCompatActivity closeButton.setOnClickListener(this); moreOptionsButton.setOnClickListener(this); + kodiButton.setOnClickListener(this); shareButton.setOnClickListener(this); toggleOrientationButton.setOnClickListener(this); switchBackgroundButton.setOnClickListener(this); @@ -593,6 +601,17 @@ public final class MainVideoPlayer extends AppCompatActivity finish(); } + public void onKodiShare() { + onPause(); + try { + NavigationHelper.playWithKore(this.context, Uri.parse( + playerImpl.getVideoUrl().replace("https", "http"))); + } catch (Exception e) { + if (DEBUG) Log.i(TAG, "Failed to start kore", e); + KoreUtil.showInstallKoreDialog(this.context); + } + } + /*////////////////////////////////////////////////////////////////////////// // Player Overrides //////////////////////////////////////////////////////////////////////////*/ @@ -619,7 +638,8 @@ public final class MainVideoPlayer extends AppCompatActivity this.getPlaybackPitch(), this.getPlaybackSkipSilence(), this.getPlaybackQuality(), - false + false, + !isPlaying() ); context.startService(intent); @@ -642,7 +662,8 @@ public final class MainVideoPlayer extends AppCompatActivity this.getPlaybackPitch(), this.getPlaybackSkipSilence(), this.getPlaybackQuality(), - false + false, + !isPlaying() ); context.startService(intent); @@ -691,6 +712,8 @@ public final class MainVideoPlayer extends AppCompatActivity } else if (v.getId() == closeButton.getId()) { onPlaybackShutdown(); return; + } else if (v.getId() == kodiButton.getId()) { + onKodiShare(); } if (getCurrentState() != STATE_COMPLETED) { diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index c0ac95445..96bc606cc 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -571,7 +571,8 @@ public final class PopupVideoPlayer extends Service { this.getPlaybackPitch(), this.getPlaybackSkipSilence(), this.getPlaybackQuality(), - false + false, + !isPlaying() ); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); @@ -1127,4 +1128,4 @@ public final class PopupVideoPlayer extends Service { return distanceFromCloseButton(popupMotionEvent) <= getClosingRadius(); } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayerActivity.java index 44fcdb8dd..b2af6d9d8 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayerActivity.java @@ -48,10 +48,7 @@ public final class PopupVideoPlayerActivity extends ServicePlayerActivity { @Override public boolean onPlayerOptionSelected(MenuItem item) { if (item.getItemId() == R.id.action_switch_background) { - this.player.setRecovery(); - getApplicationContext().sendBroadcast(getPlayerShutdownIntent()); - getApplicationContext().startService(getSwitchIntent(BackgroundPlayer.class)); - return true; + return switchTo(BackgroundPlayer.class); } return false; } diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index d1d2da344..1c6d786cd 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -164,10 +164,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS)); return true; case R.id.action_switch_main: - this.player.setRecovery(); - getApplicationContext().sendBroadcast(getPlayerShutdownIntent()); - getApplicationContext().startActivity(getSwitchIntent(MainVideoPlayer.class)); - return true; + return switchTo(MainVideoPlayer.class); } return onPlayerOptionSelected(item) || super.onOptionsItemSelected(item); } @@ -188,8 +185,17 @@ public abstract class ServicePlayerActivity extends AppCompatActivity this.player.getPlaybackPitch(), this.player.getPlaybackSkipSilence(), null, + false, false - ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying()); + } + + protected boolean switchTo(final Class clazz) { + this.player.setRecovery(); + getApplicationContext().sendBroadcast(getPlayerShutdownIntent()); + getApplicationContext().startActivity(getSwitchIntent(clazz)); + return true; } //////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java b/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java new file mode 100644 index 000000000..6f1cceeed --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java @@ -0,0 +1,95 @@ +package org.schabi.newpipe.streams; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.nodes.Node; +import org.jsoup.nodes.TextNode; +import org.jsoup.parser.Parser; +import org.jsoup.select.Elements; +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * @author kapodamy + */ +public class SrtFromTtmlWriter { + private static final String NEW_LINE = "\r\n"; + + private SharpStream out; + private boolean ignoreEmptyFrames; + private final Charset charset = StandardCharsets.UTF_8; + + private int frameIndex = 0; + + public SrtFromTtmlWriter(SharpStream out, boolean ignoreEmptyFrames) { + this.out = out; + this.ignoreEmptyFrames = ignoreEmptyFrames; + } + + private static String getTimestamp(Element frame, String attr) { + return frame + .attr(attr) + .replace('.', ',');// SRT subtitles uses comma as decimal separator + } + + private void writeFrame(String begin, String end, StringBuilder text) throws IOException { + writeString(String.valueOf(frameIndex++)); + writeString(NEW_LINE); + writeString(begin); + writeString(" --> "); + writeString(end); + writeString(NEW_LINE); + writeString(text.toString()); + writeString(NEW_LINE); + writeString(NEW_LINE); + } + + private void writeString(String text) throws IOException { + out.write(text.getBytes(charset)); + } + + public void build(SharpStream ttml) throws IOException { + /* + * TTML parser with BASIC support + * multiple CUE is not supported + * styling is not supported + * tag timestamps (in auto-generated subtitles) are not supported, maybe in the future + * also TimestampTagOption enum is not applicable + * Language parsing is not supported + */ + + // parse XML + byte[] buffer = new byte[(int) ttml.available()]; + ttml.read(buffer); + Document doc = Jsoup.parse(new ByteArrayInputStream(buffer), "UTF-8", "", Parser.xmlParser()); + + StringBuilder text = new StringBuilder(128); + Elements paragraph_list = doc.select("body > div > p"); + + // check if has frames + if (paragraph_list.size() < 1) return; + + for (Element paragraph : paragraph_list) { + text.setLength(0); + + for (Node children : paragraph.childNodes()) { + if (children instanceof TextNode) + text.append(((TextNode) children).text()); + else if (children instanceof Element && ((Element) children).tagName().equalsIgnoreCase("br")) + text.append(NEW_LINE); + } + + if (ignoreEmptyFrames && text.length() < 1) continue; + + String begin = getTimestamp(paragraph, "begin"); + String end = getTimestamp(paragraph, "end"); + + writeFrame(begin, end, text); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/streams/SubtitleConverter.java b/app/src/main/java/org/schabi/newpipe/streams/SubtitleConverter.java deleted file mode 100644 index c41db4373..000000000 --- a/app/src/main/java/org/schabi/newpipe/streams/SubtitleConverter.java +++ /dev/null @@ -1,369 +0,0 @@ -package org.schabi.newpipe.streams; - -import org.schabi.newpipe.streams.io.SharpStream; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.xml.sax.SAXException; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.nio.charset.Charset; -import java.text.ParseException; -import java.util.Locale; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.xpath.XPathExpressionException; - -/** - * @author kapodamy - */ -public class SubtitleConverter { - private static final String NEW_LINE = "\r\n"; - - public void dumpTTML(SharpStream in, final SharpStream out, final boolean ignoreEmptyFrames, final boolean detectYoutubeDuplicateLines - ) throws IOException, ParseException, SAXException, ParserConfigurationException, XPathExpressionException { - - final FrameWriter callback = new FrameWriter() { - int frameIndex = 0; - final Charset charset = Charset.forName("utf-8"); - - @Override - public void yield(SubtitleFrame frame) throws IOException { - if (ignoreEmptyFrames && frame.isEmptyText()) { - return; - } - out.write(String.valueOf(frameIndex++).getBytes(charset)); - out.write(NEW_LINE.getBytes(charset)); - out.write(getTime(frame.start, true).getBytes(charset)); - out.write(" --> ".getBytes(charset)); - out.write(getTime(frame.end, true).getBytes(charset)); - out.write(NEW_LINE.getBytes(charset)); - out.write(frame.text.getBytes(charset)); - out.write(NEW_LINE.getBytes(charset)); - out.write(NEW_LINE.getBytes(charset)); - } - }; - - read_xml_based(in, callback, detectYoutubeDuplicateLines, - "tt", "xmlns", "http://www.w3.org/ns/ttml", - new String[]{"timedtext", "head", "wp"}, - new String[]{"body", "div", "p"}, - "begin", "end", true - ); - } - - private void read_xml_based(SharpStream source, FrameWriter callback, boolean detectYoutubeDuplicateLines, - String root, String formatAttr, String formatVersion, String[] cuePath, String[] framePath, - String timeAttr, String durationAttr, boolean hasTimestamp - ) throws IOException, ParseException, SAXException, ParserConfigurationException, XPathExpressionException { - /* - * XML based subtitles parser with BASIC support - * multiple CUE is not supported - * styling is not supported - * tag timestamps (in auto-generated subtitles) are not supported, maybe in the future - * also TimestampTagOption enum is not applicable - * Language parsing is not supported - */ - - byte[] buffer = new byte[(int) source.available()]; - source.read(buffer); - - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setNamespaceAware(true); - DocumentBuilder builder = factory.newDocumentBuilder(); - Document xml = builder.parse(new ByteArrayInputStream(buffer)); - - String attr; - - // get the format version or namespace - Element node = xml.getDocumentElement(); - - if (node == null) { - throw new ParseException("Can't get the format version. ¿wrong namespace?", -1); - } else if (!node.getNodeName().equals(root)) { - throw new ParseException("Invalid root", -1); - } - - if (formatAttr.equals("xmlns")) { - if (!node.getNamespaceURI().equals(formatVersion)) { - throw new UnsupportedOperationException("Expected xml namespace: " + formatVersion); - } - } else { - attr = node.getAttributeNS(formatVersion, formatAttr); - if (attr == null) { - throw new ParseException("Can't get the format attribute", -1); - } - if (!attr.equals(formatVersion)) { - throw new ParseException("Invalid format version : " + attr, -1); - } - } - - NodeList node_list; - - int line_break = 0;// Maximum characters per line if present (valid for TranScript v3) - - if (!hasTimestamp) { - node_list = selectNodes(xml, cuePath, formatVersion); - - if (node_list != null) { - // if the subtitle has multiple CUEs, use the highest value - for (int i = 0; i < node_list.getLength(); i++) { - try { - int tmp = Integer.parseInt(((Element) node_list.item(i)).getAttributeNS(formatVersion, "ah")); - if (tmp > line_break) { - line_break = tmp; - } - } catch (Exception err) { - } - } - } - } - - // parse every frame - node_list = selectNodes(xml, framePath, formatVersion); - - if (node_list == null) { - return;// no frames detected - } - - int fs_ff = -1;// first timestamp of first frame - boolean limit_lines = false; - - for (int i = 0; i < node_list.getLength(); i++) { - Element elem = (Element) node_list.item(i); - SubtitleFrame obj = new SubtitleFrame(); - obj.text = elem.getTextContent(); - - attr = elem.getAttribute(timeAttr);// ¡this cant be null! - obj.start = hasTimestamp ? parseTimestamp(attr) : Integer.parseInt(attr); - - attr = elem.getAttribute(durationAttr); - if (obj.text == null || attr == null) { - continue;// normally is a blank line (on auto-generated subtitles) ignore - } - - if (hasTimestamp) { - obj.end = parseTimestamp(attr); - - if (detectYoutubeDuplicateLines) { - if (limit_lines) { - int swap = obj.end; - obj.end = fs_ff; - fs_ff = swap; - } else { - if (fs_ff < 0) { - fs_ff = obj.end; - } else { - if (fs_ff < obj.start) { - limit_lines = true;// the subtitles has duplicated lines - } else { - detectYoutubeDuplicateLines = false; - } - } - } - } - } else { - obj.end = obj.start + Integer.parseInt(attr); - } - - if (/*node.getAttribute("w").equals("1") &&*/line_break > 1 && obj.text.length() > line_break) { - - // implement auto line breaking (once) - StringBuilder text = new StringBuilder(obj.text); - obj.text = null; - - switch (text.charAt(line_break)) { - case ' ': - case '\t': - putBreakAt(line_break, text); - break; - default:// find the word start position - for (int j = line_break - 1; j > 0; j--) { - switch (text.charAt(j)) { - case ' ': - case '\t': - putBreakAt(j, text); - j = -1; - break; - case '\r': - case '\n': - j = -1;// long word, just ignore - break; - } - } - break; - } - - obj.text = text.toString();// set the processed text - } - - callback.yield(obj); - } - } - - private static NodeList selectNodes(Document xml, String[] path, String namespaceUri) { - Element ref = xml.getDocumentElement(); - - for (int i = 0; i < path.length - 1; i++) { - NodeList nodes = ref.getChildNodes(); - if (nodes.getLength() < 1) { - return null; - } - - Element elem; - for (int j = 0; j < nodes.getLength(); j++) { - if (nodes.item(j).getNodeType() == Node.ELEMENT_NODE) { - elem = (Element) nodes.item(j); - if (elem.getNodeName().equals(path[i]) && elem.getNamespaceURI().equals(namespaceUri)) { - ref = elem; - break; - } - } - } - } - - return ref.getElementsByTagNameNS(namespaceUri, path[path.length - 1]); - } - - private static int parseTimestamp(String multiImpl) throws NumberFormatException, ParseException { - if (multiImpl.length() < 1) { - return 0; - } else if (multiImpl.length() == 1) { - return Integer.parseInt(multiImpl) * 1000;// ¡this must be a number in seconds! - } - - // detect wallclock-time - if (multiImpl.startsWith("wallclock(")) { - throw new UnsupportedOperationException("Parsing wallclock timestamp is not implemented"); - } - - // detect offset-time - if (multiImpl.indexOf(':') < 0) { - int multiplier = 1000; - char metric = multiImpl.charAt(multiImpl.length() - 1); - switch (metric) { - case 'h': - multiplier *= 3600000; - break; - case 'm': - multiplier *= 60000; - break; - case 's': - if (multiImpl.charAt(multiImpl.length() - 2) == 'm') { - multiplier = 1;// ms - } - break; - default: - if (!Character.isDigit(metric)) { - throw new NumberFormatException("Invalid metric suffix found on : " + multiImpl); - } - metric = '\0'; - break; - } - try { - String offset_time = multiImpl; - - if (multiplier == 1) { - offset_time = offset_time.substring(0, offset_time.length() - 2); - } else if (metric != '\0') { - offset_time = offset_time.substring(0, offset_time.length() - 1); - } - - double time_metric_based = Double.parseDouble(offset_time); - if (Math.abs(time_metric_based) <= Double.MAX_VALUE) { - return (int) (time_metric_based * multiplier); - } - } catch (Exception err) { - throw new UnsupportedOperationException("Invalid or not implemented timestamp on: " + multiImpl); - } - } - - // detect clock-time - int time = 0; - String[] units = multiImpl.split(":"); - - if (units.length < 3) { - throw new ParseException("Invalid clock-time timestamp", -1); - } - - time += Integer.parseInt(units[0]) * 3600000;// hours - time += Integer.parseInt(units[1]) * 60000;//minutes - time += Float.parseFloat(units[2]) * 1000f;// seconds and milliseconds (if present) - - // frames and sub-frames are ignored (not implemented) - // time += units[3] * fps; - return time; - } - - private static void putBreakAt(int idx, StringBuilder str) { - // this should be optimized at compile time - - if (NEW_LINE.length() > 1) { - str.delete(idx, idx + 1);// remove after replace - str.insert(idx, NEW_LINE); - } else { - str.setCharAt(idx, NEW_LINE.charAt(0)); - } - } - - private static String getTime(int time, boolean comma) { - // cast every value to integer to avoid auto-round in ToString("00"). - StringBuilder str = new StringBuilder(12); - str.append(numberToString(time / 1000 / 3600, 2));// hours - str.append(':'); - str.append(numberToString(time / 1000 / 60 % 60, 2));// minutes - str.append(':'); - str.append(numberToString(time / 1000 % 60, 2));// seconds - str.append(comma ? ',' : '.'); - str.append(numberToString(time % 1000, 3));// miliseconds - - return str.toString(); - } - - private static String numberToString(int nro, int pad) { - return String.format(Locale.ENGLISH, "%0".concat(String.valueOf(pad)).concat("d"), nro); - } - - - /****************** - * helper classes * - ******************/ - - private interface FrameWriter { - - void yield(SubtitleFrame frame) throws IOException; - } - - private static class SubtitleFrame { - //Java no support unsigned int - - public int end; - public int start; - public String text = ""; - - private boolean isEmptyText() { - if (text == null) { - return true; - } - - for (int i = 0; i < text.length(); i++) { - switch (text.charAt(i)) { - case ' ': - case '\t': - case '\r': - case '\n': - break; - default: - return false; - } - } - - return true; - } - } - -} diff --git a/app/src/main/java/org/schabi/newpipe/util/KoreUtil.java b/app/src/main/java/org/schabi/newpipe/util/KoreUtil.java new file mode 100644 index 000000000..2ed3c698d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/KoreUtil.java @@ -0,0 +1,23 @@ +package org.schabi.newpipe.util; + + +import android.content.Context; +import android.content.DialogInterface; +import androidx.appcompat.app.AlertDialog; + +import org.schabi.newpipe.R; + + +public class KoreUtil { + private KoreUtil() { } + + public static void showInstallKoreDialog(final Context context) { + final AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setMessage(R.string.kore_not_found) + .setPositiveButton(R.string.install, + (DialogInterface dialog, int which) -> NavigationHelper.installKore(context)) + .setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> { + }); + builder.create().show(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index e2b03c8e8..a19aa92ae 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -109,12 +109,14 @@ public class NavigationHelper { final float playbackPitch, final boolean playbackSkipSilence, @Nullable final String playbackQuality, - final boolean resumePlayback) { + final boolean resumePlayback, + final boolean startPaused) { return getPlayerIntent(context, targetClazz, playQueue, playbackQuality, resumePlayback) .putExtra(BasePlayer.REPEAT_MODE, repeatMode) .putExtra(BasePlayer.PLAYBACK_SPEED, playbackSpeed) .putExtra(BasePlayer.PLAYBACK_PITCH, playbackPitch) - .putExtra(BasePlayer.PLAYBACK_SKIP_SILENCE, playbackSkipSilence); + .putExtra(BasePlayer.PLAYBACK_SKIP_SILENCE, playbackSkipSilence) + .putExtra(BasePlayer.START_PAUSED, startPaused); } public static void playOnMainPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) { diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java index 773ff92d1..e93e83a87 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java @@ -80,7 +80,7 @@ public abstract class Postprocessing implements Serializable { private transient DownloadMission mission; - private File tempFile; + private transient File tempFile; Postprocessing(boolean reserveSpace, boolean worksOnSameFile, String algorithmName) { this.reserveSpace = reserveSpace; @@ -95,8 +95,12 @@ public abstract class Postprocessing implements Serializable { public void cleanupTemporalDir() { if (tempFile != null && tempFile.exists()) { - //noinspection ResultOfMethodCallIgnored - tempFile.delete(); + try { + //noinspection ResultOfMethodCallIgnored + tempFile.delete(); + } catch (Exception e) { + // nothing to do + } } } diff --git a/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java b/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java index 5a5b687f7..8ed0dfae5 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java @@ -2,15 +2,10 @@ package us.shandian.giga.postprocessing; import android.util.Log; -import org.schabi.newpipe.streams.SubtitleConverter; +import org.schabi.newpipe.streams.SrtFromTtmlWriter; import org.schabi.newpipe.streams.io.SharpStream; -import org.xml.sax.SAXException; import java.io.IOException; -import java.text.ParseException; - -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.xpath.XPathExpressionException; /** * @author kapodamy @@ -27,33 +22,16 @@ class TtmlConverter extends Postprocessing { int process(SharpStream out, SharpStream... sources) throws IOException { // check if the subtitle is already in srt and copy, this should never happen String format = getArgumentAt(0, null); + boolean ignoreEmptyFrames = getArgumentAt(1, "true").equals("true"); if (format == null || format.equals("ttml")) { - SubtitleConverter ttmlDumper = new SubtitleConverter(); + SrtFromTtmlWriter writer = new SrtFromTtmlWriter(out, ignoreEmptyFrames); try { - ttmlDumper.dumpTTML( - sources[0], - out, - getArgumentAt(1, "true").equals("true"), - getArgumentAt(2, "true").equals("true") - ); + writer.build(sources[0]); } catch (Exception err) { Log.e(TAG, "subtitle parse failed", err); - - if (err instanceof IOException) { - return 1; - } else if (err instanceof ParseException) { - return 2; - } else if (err instanceof SAXException) { - return 3; - } else if (err instanceof ParserConfigurationException) { - return 4; - } else if (err instanceof XPathExpressionException) { - return 7; - } - - return 8; + return err instanceof IOException ? 1 : 8; } return OK_RESULT; diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManager.java b/app/src/main/java/us/shandian/giga/service/DownloadManager.java index e8bc468e9..994c6ee63 100644 --- a/app/src/main/java/us/shandian/giga/service/DownloadManager.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManager.java @@ -139,6 +139,9 @@ public class DownloadManager { Log.d(TAG, "Loading pending downloads from directory: " + mPendingMissionsDir.getAbsolutePath()); } + File tempDir = pickAvailableTemporalDir(ctx); + Log.i(TAG, "using '" + tempDir + "' as temporal directory"); + for (File sub : subs) { if (!sub.isFile()) continue; if (sub.getName().equals(".tmp")) continue; @@ -184,7 +187,7 @@ public class DownloadManager { if (mis.psAlgorithm != null) { mis.psAlgorithm.cleanupTemporalDir(); - mis.psAlgorithm.setTemporalDir(pickAvailableTemporalDir(ctx)); + mis.psAlgorithm.setTemporalDir(tempDir); } mis.metadata = sub; @@ -513,13 +516,21 @@ public class DownloadManager { } static File pickAvailableTemporalDir(@NonNull Context ctx) { - if (isDirectoryAvailable(ctx.getExternalFilesDir(null))) - return ctx.getExternalFilesDir(null); - else if (isDirectoryAvailable(ctx.getFilesDir())) - return ctx.getFilesDir(); + File dir = ctx.getExternalFilesDir(null); + if (isDirectoryAvailable(dir)) return dir; + + dir = ctx.getFilesDir(); + if (isDirectoryAvailable(dir)) return dir; // this never should happen - return ctx.getDir("tmp", Context.MODE_PRIVATE); + dir = ctx.getDir("muxing_tmp", Context.MODE_PRIVATE); + if (isDirectoryAvailable(dir)) return dir; + + // fallback to cache dir + dir = ctx.getCacheDir(); + if (isDirectoryAvailable(dir)) return dir; + + throw new RuntimeException("Not temporal directories are available"); } @Nullable diff --git a/app/src/main/res/layout-large-land/activity_main_player.xml b/app/src/main/res/layout-large-land/activity_main_player.xml index b535db2b8..98017b132 100644 --- a/app/src/main/res/layout-large-land/activity_main_player.xml +++ b/app/src/main/res/layout-large-land/activity_main_player.xml @@ -305,7 +305,7 @@ tools:text="English" /> + + + +