Merge remote-tracking branch 'github/Start-screen-hint' into Start-screen-hint
# Conflicts: # app/src/main/java/org/schabi/newpipe/VideoItemListActivity.java
| @@ -20,6 +20,7 @@ android: | ||||
| env: | ||||
|   global: | ||||
|     - ADB_INSTALL_TIMEOUT=8 # minutes (2 by default) | ||||
|     - GRADLE_OPTS=-Xmx512m # give gradle more memory since it seem to fail otherwise | ||||
|   matrix: | ||||
|     - ANDROID_TARGET=android-19  ANDROID_ABI=armeabi-v7a | ||||
|  | ||||
| @@ -28,3 +29,5 @@ before_script: | ||||
|   - emulator -avd test -no-skin -no-audio -no-window & | ||||
|   - android-wait-for-emulator | ||||
|   - adb shell input keyevent 82 & | ||||
|  | ||||
| script: ./gradlew --info build connectedCheck | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| # NewPipe | ||||
| NewPipe: A free lightweight Youtube frontend for Android.   | ||||
|  | ||||
| [](http://dasochan.nl/newpipe/) | ||||
| [](https://newpipe.schabi.org) | ||||
|  | ||||
| Project status: | ||||
| [](https://hosted.weblate.org/engage/NewPipe/) | ||||
| @@ -11,6 +11,12 @@ Project status: | ||||
|  | ||||
| [](https://f-droid.org/repository/browse/?fdfilter=newpipe&fdid=org.schabi.newpipe) | ||||
|  | ||||
| ## Donate | ||||
|  | ||||
| `16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh` | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Screenshots | ||||
|  | ||||
| [<img src="screenshots/screenshot_1.png" width=150>](screenshots/screenshot_1.png) | ||||
| @@ -36,6 +42,7 @@ NewPipe does not use any Google framework libraries, or the YouTube API. It only | ||||
| * Show Next/Related videos | ||||
| * Search YouTube in a specific language | ||||
| * Orbot/Tor support (no streaming yet, experimental) | ||||
| * Watch age restricted material | ||||
|  | ||||
| ### Coming Features | ||||
|  | ||||
|   | ||||
| @@ -8,8 +8,8 @@ android { | ||||
|         applicationId "org.schabi.newpipe" | ||||
|         minSdkVersion 15 | ||||
|         targetSdkVersion 23 | ||||
|         versionCode 13 | ||||
|         versionName "0.7.4" | ||||
|         versionCode 16 | ||||
|         versionName "0.7.7" | ||||
|     } | ||||
|     buildTypes { | ||||
|         release { | ||||
| @@ -32,14 +32,16 @@ android { | ||||
|  | ||||
| dependencies { | ||||
|     compile fileTree(include: ['*.jar'], dir: 'libs') | ||||
|     compile 'com.android.support:appcompat-v7:23.1.1' | ||||
|     compile 'com.android.support:support-v4:23.1.1' | ||||
|     compile 'com.android.support:design:23.1.1' | ||||
|     compile 'com.android.support:recyclerview-v7:23.1.1' | ||||
|     compile 'com.android.support:appcompat-v7:23.2.0' | ||||
|     compile 'com.android.support:support-v4:23.2.0' | ||||
|     compile 'com.android.support:design:23.2.0' | ||||
|     compile 'com.android.support:recyclerview-v7:23.2.0' | ||||
|     compile 'org.jsoup:jsoup:1.8.3' | ||||
|     compile 'org.mozilla:rhino:1.7.7' | ||||
|     compile 'info.guardianproject.netcipher:netcipher:1.2' | ||||
|     compile 'de.hdodenhof:circleimageview:2.0.0' | ||||
|     compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5' | ||||
|     compile 'com.github.nirhart:parallaxscroll:1.0' | ||||
|     compile 'org.apache.directory.studio:org.apache.commons.lang:2.6' | ||||
|     compile 'com.google.android.exoplayer:exoplayer:r1.5.5' | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,121 @@ | ||||
| package org.schabi.newpipe.extractor.youtube; | ||||
|  | ||||
| import android.test.AndroidTestCase; | ||||
|  | ||||
| import org.apache.commons.lang.exception.ExceptionUtils; | ||||
| import org.schabi.newpipe.extractor.AbstractVideoInfo; | ||||
| import org.schabi.newpipe.extractor.SearchResult; | ||||
| import org.schabi.newpipe.extractor.ServiceList; | ||||
| import org.schabi.newpipe.extractor.StreamPreviewInfo; | ||||
| import org.schabi.newpipe.extractor.SearchEngine; | ||||
| import org.schabi.newpipe.extractor.services.youtube.YoutubeSearchEngine; | ||||
| import org.schabi.newpipe.Downloader; | ||||
| import org.schabi.newpipe.extractor.services.youtube.YoutubeService; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
|  | ||||
| /** | ||||
|  * Created by Christian Schabesberger on 29.12.15. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org> | ||||
|  * YoutubeSearchEngineTest.java is part of NewPipe. | ||||
|  * | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * NewPipe is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| public class YoutubeSearchEngineTest extends AndroidTestCase { | ||||
|     private SearchResult result; | ||||
|     private ArrayList<String> suggestionReply; | ||||
|  | ||||
|     @Override | ||||
|     public void setUp() throws Exception{ | ||||
|         super.setUp(); | ||||
|         SearchEngine engine = ServiceList.getService("Youtube") | ||||
|                 .getSearchEngineInstance(new Downloader()); | ||||
|  | ||||
|         result = engine.search("lefloid", | ||||
|                 0, "de", new Downloader()).getSearchResult(); | ||||
|         suggestionReply = engine.suggestionList("hello","de",new Downloader()); | ||||
|     } | ||||
|  | ||||
|     public void testIfNoErrorOccur() { | ||||
|         assertTrue(result.errors.isEmpty() ? "" : ExceptionUtils.getStackTrace(result.errors.get(0)) | ||||
|                 ,result.errors.isEmpty()); | ||||
|     } | ||||
|  | ||||
|     public void testIfListIsNotEmpty() { | ||||
|         assertEquals(result.resultList.size() > 0, true); | ||||
|     } | ||||
|  | ||||
|     public void testItemsHaveTitle() { | ||||
|         for(StreamPreviewInfo i : result.resultList) { | ||||
|             assertEquals(i.title.isEmpty(), false); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void testItemsHaveUploader() { | ||||
|         for(StreamPreviewInfo i : result.resultList) { | ||||
|             assertEquals(i.uploader.isEmpty(), false); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void testItemsHaveRightDuration() { | ||||
|         for(StreamPreviewInfo i : result.resultList) { | ||||
|             assertTrue(i.duration >= 0); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void testItemsHaveRightThumbnail() { | ||||
|         for (StreamPreviewInfo i : result.resultList) { | ||||
|             assertTrue(i.thumbnail_url, i.thumbnail_url.contains("https://")); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void testItemsHaveRightVideoUrl() { | ||||
|         for (StreamPreviewInfo i : result.resultList) { | ||||
|             assertTrue(i.webpage_url, i.webpage_url.contains("https://")); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void testViewCount() { | ||||
|         /* | ||||
|         for(StreamPreviewInfo i : result.resultList) { | ||||
|             assertTrue(Long.toString(i.view_count), i.view_count != -1); | ||||
|         } | ||||
|         */ | ||||
|         // that specific link used for this test, there are no videos with less | ||||
|         // than 10.000 views, so we can test against that. | ||||
|         for(StreamPreviewInfo i : result.resultList) { | ||||
|             assertTrue(i.title + ": " + Long.toString(i.view_count), i.view_count >= 10000); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void testStreamType() { | ||||
|         for(StreamPreviewInfo i : result.resultList) { | ||||
|             assertTrue("not a livestream and not a video", | ||||
|                     i.stream_type == AbstractVideoInfo.StreamType.VIDEO_STREAM || | ||||
|                     i.stream_type == AbstractVideoInfo.StreamType.LIVE_STREAM); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void testIfSuggestionsAreReplied() { | ||||
|         assertEquals(!suggestionReply.isEmpty(), true); | ||||
|     } | ||||
|  | ||||
|     public void testIfSuggestionsAreValid() { | ||||
|         for(String s : suggestionReply) { | ||||
|             assertTrue(s, !s.isEmpty()); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,17 +1,19 @@ | ||||
| package org.schabi.newpipe.services.youtube; | ||||
| package org.schabi.newpipe.extractor.youtube; | ||||
| 
 | ||||
| import android.test.AndroidTestCase; | ||||
| 
 | ||||
| import org.schabi.newpipe.Downloader; | ||||
| import org.schabi.newpipe.crawler.CrawlingException; | ||||
| import org.schabi.newpipe.crawler.ParsingException; | ||||
| import org.schabi.newpipe.crawler.services.youtube.YoutubeStreamExtractor; | ||||
| import org.schabi.newpipe.crawler.VideoInfo; | ||||
| import org.schabi.newpipe.extractor.AbstractVideoInfo; | ||||
| import org.schabi.newpipe.extractor.ExtractionException; | ||||
| import org.schabi.newpipe.extractor.ParsingException; | ||||
| import org.schabi.newpipe.extractor.ServiceList; | ||||
| import org.schabi.newpipe.extractor.StreamExtractor; | ||||
| import org.schabi.newpipe.extractor.VideoStream; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| 
 | ||||
| /** | ||||
|  * Created by the-scrabi on 30.12.15. | ||||
|  * Created by Christian Schabesberger on 30.12.15. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org> | ||||
|  * YoutubeVideoExtractorDefault.java is part of NewPipe. | ||||
| @@ -31,15 +33,11 @@ import java.io.IOException; | ||||
|  */ | ||||
| 
 | ||||
| public class YoutubeStreamExtractorDefaultTest extends AndroidTestCase { | ||||
|     private YoutubeStreamExtractor extractor; | ||||
|     private StreamExtractor extractor; | ||||
| 
 | ||||
|     public void setUp() throws IOException, CrawlingException { | ||||
|         /* some anonymus video test | ||||
|         extractor = new YoutubeStreamExtractor("https://www.youtube.com/watch?v=FmG385_uUys", | ||||
|                 new Downloader()); */ | ||||
|         /* some vevo video (suggested to test against) */ | ||||
|         extractor = new YoutubeStreamExtractor("https://www.youtube.com/watch?v=YQHsXMglC9A", | ||||
|                 new Downloader()); | ||||
|     public void setUp() throws IOException, ExtractionException { | ||||
|         extractor = ServiceList.getService("Youtube") | ||||
|                 .getExtractorInstance("https://www.youtube.com/watch?v=YQHsXMglC9A", new Downloader()); | ||||
|     } | ||||
| 
 | ||||
|     public void testGetInvalidTimeStamp() throws ParsingException { | ||||
| @@ -47,9 +45,10 @@ public class YoutubeStreamExtractorDefaultTest extends AndroidTestCase { | ||||
|                 extractor.getTimeStamp() <= 0); | ||||
|     } | ||||
| 
 | ||||
|     public void testGetValidTimeStamp() throws CrawlingException, IOException { | ||||
|         YoutubeStreamExtractor extractor = | ||||
|                 new YoutubeStreamExtractor("https://youtu.be/FmG385_uUys?t=174", new Downloader()); | ||||
|     public void testGetValidTimeStamp() throws ExtractionException, IOException { | ||||
|         StreamExtractor extractor = | ||||
|                 ServiceList.getService("Youtube") | ||||
|                         .getExtractorInstance("https://youtu.be/FmG385_uUys?t=174", new Downloader()); | ||||
|         assertTrue(Integer.toString(extractor.getTimeStamp()), | ||||
|                 extractor.getTimeStamp() == 174); | ||||
|     } | ||||
| @@ -70,8 +69,9 @@ public class YoutubeStreamExtractorDefaultTest extends AndroidTestCase { | ||||
|         assertTrue(extractor.getLength() > 0); | ||||
|     } | ||||
| 
 | ||||
|     public void testGetViews() throws ParsingException { | ||||
|         assertTrue(extractor.getLength() > 0); | ||||
|     public void testGetViewCount() throws ParsingException { | ||||
|         assertTrue(Long.toString(extractor.getViewCount()), | ||||
|                 extractor.getViewCount() > /* specific to that video */ 1224000074); | ||||
|     } | ||||
| 
 | ||||
|     public void testGetUploadDate() throws ParsingException { | ||||
| @@ -93,7 +93,7 @@ public class YoutubeStreamExtractorDefaultTest extends AndroidTestCase { | ||||
|     } | ||||
| 
 | ||||
|     public void testGetVideoStreams() throws ParsingException { | ||||
|         for(VideoInfo.VideoStream s : extractor.getVideoStreams()) { | ||||
|         for(VideoStream s : extractor.getVideoStreams()) { | ||||
|             assertTrue(s.url, | ||||
|                     s.url.contains("https://")); | ||||
|             assertTrue(s.resolution.length() > 0); | ||||
| @@ -102,8 +102,12 @@ public class YoutubeStreamExtractorDefaultTest extends AndroidTestCase { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public void testStreamType() throws ParsingException { | ||||
|         assertTrue(extractor.getStreamType() == AbstractVideoInfo.StreamType.VIDEO_STREAM); | ||||
|     } | ||||
| 
 | ||||
|     public void testGetDashMpd() throws ParsingException { | ||||
|         assertTrue(extractor.getDashMpdUrl(), | ||||
|                 !extractor.getDashMpdUrl().isEmpty()); | ||||
|                 extractor.getDashMpdUrl() != null || !extractor.getDashMpdUrl().isEmpty()); | ||||
|     } | ||||
| } | ||||
| @@ -1,15 +1,16 @@ | ||||
| package org.schabi.newpipe.services.youtube; | ||||
| package org.schabi.newpipe.extractor.youtube; | ||||
| 
 | ||||
| import android.test.AndroidTestCase; | ||||
| 
 | ||||
| import org.schabi.newpipe.Downloader; | ||||
| import org.schabi.newpipe.crawler.CrawlingException; | ||||
| import org.schabi.newpipe.crawler.services.youtube.YoutubeStreamExtractor; | ||||
| import org.schabi.newpipe.extractor.ExtractionException; | ||||
| import org.schabi.newpipe.extractor.ServiceList; | ||||
| import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| 
 | ||||
| /** | ||||
|  * Created by the-scrabi on 30.12.15. | ||||
|  * Created by Christian Schabesberger on 30.12.15. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org> | ||||
|  * YoutubeVideoExtractorGema.java is part of NewPipe. | ||||
| @@ -35,12 +36,12 @@ public class YoutubeStreamExtractorGemaTest extends AndroidTestCase { | ||||
|     // Deaktivate this Test Case bevore uploading it githup, otherwise CI will fail. | ||||
|     private static final boolean testActive = false; | ||||
| 
 | ||||
|     public void testGemaError() throws IOException, CrawlingException { | ||||
|     public void testGemaError() throws IOException, ExtractionException { | ||||
|         if(testActive) { | ||||
|             try { | ||||
|                 new YoutubeStreamExtractor("https://www.youtube.com/watch?v=3O1_3zBUKM8", | ||||
|                 ServiceList.getService("Youtube") | ||||
|                         .getExtractorInstance("https://www.youtube.com/watch?v=3O1_3zBUKM8", | ||||
|                         new Downloader()); | ||||
|                 assertTrue("Gema exception not thrown", false); | ||||
|             } catch(YoutubeStreamExtractor.GemaException ge) { | ||||
|                 assertTrue(true); | ||||
|             } | ||||
| @@ -0,0 +1,87 @@ | ||||
| package org.schabi.newpipe.extractor.youtube; | ||||
|  | ||||
| import android.test.AndroidTestCase; | ||||
|  | ||||
| import org.schabi.newpipe.Downloader; | ||||
| import org.schabi.newpipe.extractor.ExtractionException; | ||||
| import org.schabi.newpipe.extractor.ParsingException; | ||||
| import org.schabi.newpipe.extractor.ServiceList; | ||||
| import org.schabi.newpipe.extractor.StreamExtractor; | ||||
| import org.schabi.newpipe.extractor.VideoStream; | ||||
|  | ||||
| import java.io.IOException; | ||||
|  | ||||
| public class YoutubeStreamExtractorRestrictedTest extends AndroidTestCase { | ||||
|     private StreamExtractor extractor; | ||||
|  | ||||
|     public void setUp() throws IOException, ExtractionException { | ||||
|         extractor = ServiceList.getService("Youtube") | ||||
|                 .getExtractorInstance("https://www.youtube.com/watch?v=i6JTvzrpBy0", | ||||
|                         new Downloader()); | ||||
|     } | ||||
|  | ||||
|     public void testGetInvalidTimeStamp() throws ParsingException { | ||||
|         assertTrue(Integer.toString(extractor.getTimeStamp()), | ||||
|                 extractor.getTimeStamp() <= 0); | ||||
|     } | ||||
|  | ||||
|     public void testGetValidTimeStamp() throws ExtractionException, IOException { | ||||
|         StreamExtractor extractor=ServiceList.getService("Youtube") | ||||
|                 .getExtractorInstance("https://youtu.be/FmG385_uUys?t=174", | ||||
|                         new Downloader()); | ||||
|         assertTrue(Integer.toString(extractor.getTimeStamp()), | ||||
|                 extractor.getTimeStamp() == 174); | ||||
|     } | ||||
|  | ||||
|     public void testGetAgeLimit() throws ParsingException { | ||||
|         assertTrue(extractor.getAgeLimit() == 18); | ||||
|     } | ||||
|  | ||||
|     public void testGetTitle() throws ParsingException { | ||||
|         assertTrue(!extractor.getTitle().isEmpty()); | ||||
|     } | ||||
|  | ||||
|     public void testGetDescription() throws ParsingException { | ||||
|         assertTrue(extractor.getDescription() != null); | ||||
|     } | ||||
|  | ||||
|     public void testGetUploader() throws ParsingException { | ||||
|         assertTrue(!extractor.getUploader().isEmpty()); | ||||
|     } | ||||
|  | ||||
|     public void testGetLength() throws ParsingException { | ||||
|         assertTrue(extractor.getLength() > 0); | ||||
|     } | ||||
|  | ||||
|     public void testGetViews() throws ParsingException { | ||||
|         assertTrue(extractor.getLength() > 0); | ||||
|     } | ||||
|  | ||||
|     public void testGetUploadDate() throws ParsingException { | ||||
|         assertTrue(extractor.getUploadDate().length() > 0); | ||||
|     } | ||||
|  | ||||
|     public void testGetThumbnailUrl() throws ParsingException { | ||||
|         assertTrue(extractor.getThumbnailUrl(), | ||||
|                 extractor.getThumbnailUrl().contains("https://")); | ||||
|     } | ||||
|  | ||||
|     public void testGetUploaderThumbnailUrl() throws ParsingException { | ||||
|         assertTrue(extractor.getUploaderThumbnailUrl(), | ||||
|                 extractor.getUploaderThumbnailUrl().contains("https://")); | ||||
|     } | ||||
|  | ||||
|     public void testGetAudioStreams() throws ParsingException { | ||||
|         assertTrue(!extractor.getAudioStreams().isEmpty()); | ||||
|     } | ||||
|  | ||||
|     public void testGetVideoStreams() throws ParsingException { | ||||
|         for(VideoStream s : extractor.getVideoStreams()) { | ||||
|             assertTrue(s.url, | ||||
|                     s.url.contains("https://")); | ||||
|             assertTrue(s.resolution.length() > 0); | ||||
|             assertTrue(Integer.toString(s.format), | ||||
|                     0 <= s.format && s.format <= 4); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,52 @@ | ||||
| package org.schabi.newpipe.extractor.youtube; | ||||
|  | ||||
| import android.test.AndroidTestCase; | ||||
|  | ||||
| import org.schabi.newpipe.Downloader; | ||||
| import org.schabi.newpipe.extractor.AbstractVideoInfo; | ||||
| import org.schabi.newpipe.extractor.ExtractionException; | ||||
| import org.schabi.newpipe.extractor.ParsingException; | ||||
| import org.schabi.newpipe.extractor.ServiceList; | ||||
| import org.schabi.newpipe.extractor.StreamExtractor; | ||||
|  | ||||
| import java.io.IOException; | ||||
|  | ||||
| /** | ||||
|  * Created by Christian Schabesberger on 11.03.16. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org> | ||||
|  * YoutubestreamExtractorLiveStreamTest.java is part of NewPipe. | ||||
|  * | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * NewPipe is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
|  | ||||
| public class YoutubestreamExtractorLiveStreamTest extends AndroidTestCase { | ||||
|  | ||||
|     private StreamExtractor extractor; | ||||
|  | ||||
|     public void setUp() throws IOException, ExtractionException { | ||||
|         //todo: make the extractor not throw over a livestream | ||||
|         /* | ||||
|         extractor = ServiceList.getService("Youtube") | ||||
|                 .getExtractorInstance("https://www.youtube.com/watch?v=J0s6NjqdjLE", new Downloader()); | ||||
|                 */ | ||||
|     } | ||||
|  | ||||
|     public void testStreamType() throws ParsingException { | ||||
|         assertTrue(true); | ||||
|         // assertTrue(extractor.getStreamType() == AbstractVideoInfo.StreamType.LIVE_STREAM); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -1,93 +0,0 @@ | ||||
| package org.schabi.newpipe.services.youtube; | ||||
|  | ||||
| import android.test.AndroidTestCase; | ||||
|  | ||||
| import org.schabi.newpipe.crawler.VideoPreviewInfo; | ||||
| import org.schabi.newpipe.crawler.SearchEngine; | ||||
| import org.schabi.newpipe.crawler.services.youtube.YoutubeSearchEngine; | ||||
| import org.schabi.newpipe.Downloader; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
|  | ||||
| /** | ||||
|  * Created by the-scrabi on 29.12.15. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org> | ||||
|  * YoutubeSearchEngineTest.java is part of NewPipe. | ||||
|  * | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * NewPipe is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| public class YoutubeSearchEngineTest extends AndroidTestCase { | ||||
|     private SearchEngine.Result result; | ||||
|     private ArrayList<String> suggestionReply; | ||||
|  | ||||
|     @Override | ||||
|     public void setUp() throws Exception{ | ||||
|         super.setUp(); | ||||
|         SearchEngine engine = new YoutubeSearchEngine(); | ||||
|  | ||||
|         result = engine.search("https://www.youtube.com/results?search_query=bla", | ||||
|                 0, "de", new Downloader()); | ||||
|         suggestionReply = engine.suggestionList("hello", new Downloader()); | ||||
|     } | ||||
|  | ||||
|     public void testIfNoErrorOccur() { | ||||
|         assertEquals(result.errorMessage, ""); | ||||
|     } | ||||
|  | ||||
|     public void testIfListIsNotEmpty() { | ||||
|         assertEquals(result.resultList.size() > 0, true); | ||||
|     } | ||||
|  | ||||
|     public void testItemsHaveTitle() { | ||||
|         for(VideoPreviewInfo i : result.resultList) { | ||||
|             assertEquals(i.title.isEmpty(), false); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void testItemsHaveUploader() { | ||||
|         for(VideoPreviewInfo i : result.resultList) { | ||||
|             assertEquals(i.uploader.isEmpty(), false); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void testItemsHaveRightDuration() { | ||||
|         for(VideoPreviewInfo i : result.resultList) { | ||||
|             assertTrue(i.duration, i.duration.contains(":")); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void testItemsHaveRightThumbnail() { | ||||
|         for (VideoPreviewInfo i : result.resultList) { | ||||
|             assertTrue(i.thumbnail_url, i.thumbnail_url.contains("https://")); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void testItemsHaveRightVideoUrl() { | ||||
|         for (VideoPreviewInfo i : result.resultList) { | ||||
|             assertTrue(i.webpage_url, i.webpage_url.contains("https://")); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void testIfSuggestionsAreReplied() { | ||||
|         assertEquals(suggestionReply.size() > 0, true); | ||||
|     } | ||||
|  | ||||
|     public void testIfSuggestionsAreValid() { | ||||
|         for(String s : suggestionReply) { | ||||
|             assertTrue(s, !s.isEmpty()); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -2,22 +2,23 @@ | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     package="org.schabi.newpipe"> | ||||
|  | ||||
|     <uses-permission android:name="android.permission.INTERNET" /> | ||||
|     <uses-permission android:name="android.permission.WAKE_LOCK" /> | ||||
|     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> | ||||
|     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> | ||||
|  | ||||
|     <application | ||||
|         android:name=".App" | ||||
|         android:allowBackup="true" | ||||
|         android:icon="@mipmap/ic_launcher" | ||||
|         android:logo="@mipmap/ic_launcher" | ||||
|         android:label="@string/app_name" | ||||
|         android:logo="@mipmap/ic_launcher" | ||||
|         android:theme="@style/AppTheme" | ||||
|         tools:ignore="AllowBackup"> | ||||
|         <activity | ||||
|             android:name=".VideoItemListActivity" | ||||
|             android:label="@string/app_name" | ||||
|             android:configChanges="orientation|screenSize"> | ||||
|             android:label="@string/app_name"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.MAIN" /> | ||||
|  | ||||
| @@ -27,9 +28,7 @@ | ||||
|         <activity | ||||
|             android:name=".VideoItemDetailActivity" | ||||
|             android:label="@string/title_videoitem_detail" | ||||
|             android:theme="@style/AppTheme" | ||||
|             android:configChanges="orientation|screenSize" | ||||
|             android:screenOrientation="portrait"> | ||||
|             android:theme="@style/AppTheme"> | ||||
|             <meta-data | ||||
|                 android:name="android.support.PARENT_ACTIVITY" | ||||
|                 android:value=".VideoItemListActivity" /> | ||||
| @@ -49,6 +48,7 @@ | ||||
|                 <data android:host="www.youtube.com" /> | ||||
|                 <data android:pathPrefix="/v/" /> | ||||
|                 <data android:pathPrefix="/watch" /> | ||||
|                 <data android:pathPrefix="/attribution_link" /> | ||||
|             </intent-filter> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.VIEW" /> | ||||
| @@ -75,20 +75,40 @@ | ||||
|                 <data android:scheme="vnd.youtube.launch" /> | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|         <activity android:name=".PlayVideoActivity" | ||||
|         <activity | ||||
|             android:name=".player.PlayVideoActivity" | ||||
|             android:configChanges="orientation|keyboardHidden|screenSize" | ||||
|             android:theme="@style/VideoPlayerTheme" | ||||
|             android:parentActivityName=".VideoItemDetailActivity" | ||||
|             tools:ignore="UnusedAttribute"> | ||||
|             tools:ignore="UnusedAttribute"/> | ||||
|         <service | ||||
|             android:name=".player.BackgroundPlayer" | ||||
|             android:exported="false" | ||||
|             android:label="@string/background_player_name"/> | ||||
| 		<activity | ||||
|             android:name=".player.ExoPlayerActivity" | ||||
|             android:configChanges="keyboard|keyboardHidden|orientation|screenSize" | ||||
|             android:label="@string/app_name" | ||||
|             android:launchMode="singleInstance" | ||||
|             android:theme="@style/PlayerTheme"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="org.schabi.newpipe.exoplayer.action.VIEW" /> | ||||
|  | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|  | ||||
|                 <data android:scheme="http" /> | ||||
|                 <data android:scheme="https" /> | ||||
|                 <data android:scheme="content" /> | ||||
|                 <data android:scheme="asset" /> | ||||
|                 <data android:scheme="file" /> | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|         <service | ||||
|             android:name=".BackgroundPlayer" | ||||
|             android:name=".player.BackgroundPlayer" | ||||
|             android:label="@string/background_player_name" | ||||
|             android:exported="false" /> | ||||
|         <activity | ||||
|             android:name=".SettingsActivity" | ||||
|             android:label="@string/settings_activity_title" > | ||||
|         </activity> | ||||
|             android:label="@string/settings_activity_title" /> | ||||
|         <activity | ||||
|             android:name=".PanicResponderActivity" | ||||
|             android:launchMode="singleInstance" | ||||
| @@ -96,11 +116,14 @@ | ||||
|             android:theme="@android:style/Theme.NoDisplay"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="info.guardianproject.panic.action.TRIGGER" /> | ||||
|  | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|         <activity | ||||
|             android:name=".ExitActivity" | ||||
|             android:label="@string/general_error" | ||||
|             android:theme="@android:style/Theme.NoDisplay" /> | ||||
|         <activity android:name=".ErrorActivity"/> | ||||
|     </application> | ||||
| </manifest> | ||||
|   | ||||
| @@ -1,24 +1,19 @@ | ||||
| package org.schabi.newpipe; | ||||
|  | ||||
| import android.content.DialogInterface; | ||||
| import android.content.Intent; | ||||
| import android.content.SharedPreferences; | ||||
| import android.graphics.Bitmap; | ||||
| import android.net.Uri; | ||||
| import android.os.Bundle; | ||||
| import android.preference.PreferenceManager; | ||||
| import android.support.v7.app.ActionBar; | ||||
| import android.support.v7.app.AlertDialog; | ||||
| import android.support.v7.app.AppCompatActivity; | ||||
| import android.util.Log; | ||||
| import android.view.Menu; | ||||
| import android.view.MenuInflater; | ||||
| import android.view.MenuItem; | ||||
| import android.widget.ArrayAdapter; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| import org.schabi.newpipe.crawler.MediaFormat; | ||||
| import org.schabi.newpipe.crawler.VideoInfo; | ||||
| import org.schabi.newpipe.extractor.MediaFormat; | ||||
| import org.schabi.newpipe.extractor.StreamInfo; | ||||
| import org.schabi.newpipe.extractor.VideoStream; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| @@ -51,13 +46,16 @@ class ActionBarHandler { | ||||
|  | ||||
|     private SharedPreferences defaultPreferences = null; | ||||
|  | ||||
|     private Menu menu; | ||||
|  | ||||
|     // Only callbacks are listed here, there are more actions which don't need a callback. | ||||
|     // those are edited directly. Typically VideoItemDetailFragment will implement those callbacks. | ||||
|     private OnActionListener onShareListener; | ||||
|     private OnActionListener onOpenInBrowserListener; | ||||
|     private OnActionListener onDownloadListener; | ||||
|     private OnActionListener onPlayWithKodiListener; | ||||
|     private OnActionListener onPlayAudioListener; | ||||
|     private OnActionListener onShareListener = null; | ||||
|     private OnActionListener onOpenInBrowserListener = null; | ||||
|     private OnActionListener onDownloadListener = null; | ||||
|     private OnActionListener onPlayWithKodiListener = null; | ||||
|     private OnActionListener onPlayAudioListener = null; | ||||
|  | ||||
|  | ||||
|     // Triggered when a stream related action is triggered. | ||||
|     public interface OnActionListener { | ||||
| @@ -78,7 +76,7 @@ class ActionBarHandler { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void setupStreamList(final List<VideoInfo.VideoStream> videoStreams) { | ||||
|     public void setupStreamList(final List<VideoStream> videoStreams) { | ||||
|         if (activity != null) { | ||||
|             selectedVideoStream = 0; | ||||
|  | ||||
| @@ -86,7 +84,7 @@ class ActionBarHandler { | ||||
|             // this array will be shown in the dropdown menu for selecting the stream/resolution. | ||||
|             String[] itemArray = new String[videoStreams.size()]; | ||||
|             for (int i = 0; i < videoStreams.size(); i++) { | ||||
|                 VideoInfo.VideoStream item = videoStreams.get(i); | ||||
|                 VideoStream item = videoStreams.get(i); | ||||
|                 itemArray[i] = MediaFormat.getNameById(item.format) + " " + item.resolution; | ||||
|             } | ||||
|             int defaultResolution = getDefaultResolution(videoStreams); | ||||
| @@ -111,13 +109,13 @@ class ActionBarHandler { | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private int getDefaultResolution(final List<VideoInfo.VideoStream> videoStreams) { | ||||
|     private int getDefaultResolution(final List<VideoStream> videoStreams) { | ||||
|         String defaultResolution = defaultPreferences | ||||
|                 .getString(activity.getString(R.string.default_resolution_key), | ||||
|                         activity.getString(R.string.default_resolution_value)); | ||||
|  | ||||
|         for (int i = 0; i < videoStreams.size(); i++) { | ||||
|             VideoInfo.VideoStream item = videoStreams.get(i); | ||||
|             VideoStream item = videoStreams.get(i); | ||||
|             if (defaultResolution.equals(item.resolution)) { | ||||
|                 return i; | ||||
|             } | ||||
| @@ -128,15 +126,15 @@ class ActionBarHandler { | ||||
|     } | ||||
|  | ||||
|     public void setupMenu(Menu menu, MenuInflater inflater) { | ||||
|         this.menu = menu; | ||||
|  | ||||
|         // CAUTION set item properties programmatically otherwise it would not be accepted by | ||||
|         // appcompat itemsinflater.inflate(R.menu.videoitem_detail, menu); | ||||
|  | ||||
|         defaultPreferences = PreferenceManager.getDefaultSharedPreferences(activity); | ||||
|  | ||||
|         inflater.inflate(R.menu.videoitem_detail, menu); | ||||
|         MenuItem castItem = menu.findItem(R.id.action_play_with_kodi); | ||||
|  | ||||
|         castItem.setVisible(defaultPreferences | ||||
|         showPlayWithKodiAction(defaultPreferences | ||||
|                 .getBoolean(activity.getString(R.string.show_play_with_kodi_key), false)); | ||||
|     } | ||||
|  | ||||
| @@ -151,15 +149,21 @@ class ActionBarHandler { | ||||
|                     intent.setType("text/plain"); | ||||
|                     activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.share_dialog_title))); | ||||
|                     */ | ||||
|                 if(onShareListener != null) { | ||||
|                     onShareListener.onActionSelected(selectedVideoStream); | ||||
|                 } | ||||
|                 return true; | ||||
|             } | ||||
|             case R.id.menu_item_openInBrowser: { | ||||
|                 if(onOpenInBrowserListener != null) { | ||||
|                     onOpenInBrowserListener.onActionSelected(selectedVideoStream); | ||||
|                 } | ||||
|             } | ||||
|             return true; | ||||
|             case R.id.menu_item_download: | ||||
|                 if(onDownloadListener != null) { | ||||
|                     onDownloadListener.onActionSelected(selectedVideoStream); | ||||
|                 } | ||||
|                 return true; | ||||
|             case R.id.action_settings: { | ||||
|                 Intent intent = new Intent(activity, SettingsActivity.class); | ||||
| @@ -167,10 +171,14 @@ class ActionBarHandler { | ||||
|                 return true; | ||||
|             } | ||||
|             case R.id.action_play_with_kodi: | ||||
|                 if(onPlayWithKodiListener != null) { | ||||
|                     onPlayWithKodiListener.onActionSelected(selectedVideoStream); | ||||
|                 } | ||||
|                 return true; | ||||
|             case R.id.menu_item_play_audio: | ||||
|                 if(onPlayAudioListener != null) { | ||||
|                     onPlayAudioListener.onActionSelected(selectedVideoStream); | ||||
|                 } | ||||
|                 return true; | ||||
|             default: | ||||
|                 Log.e(TAG, "Menu Item not known"); | ||||
| @@ -201,4 +209,16 @@ class ActionBarHandler { | ||||
|     public void setOnPlayAudioListener(OnActionListener listener) { | ||||
|         onPlayAudioListener = listener; | ||||
|     } | ||||
|  | ||||
|     public void showAudioAction(boolean visible) { | ||||
|         menu.findItem(R.id.menu_item_play_audio).setVisible(visible); | ||||
|     } | ||||
|  | ||||
|     public void showDownloadAction(boolean visible) { | ||||
|         menu.findItem(R.id.menu_item_download).setVisible(visible); | ||||
|     } | ||||
|  | ||||
|     public void showPlayWithKodiAction(boolean visible) { | ||||
|         menu.findItem(R.id.action_play_with_kodi).setVisible(visible); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -22,10 +22,12 @@ package org.schabi.newpipe; | ||||
|  | ||||
| import android.graphics.Bitmap; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| /** | ||||
|  * Singleton: | ||||
|  * Used to send data between certain Activity/Services within the same process. | ||||
|  * This can be considered as hack inside the Android universe. **/ | ||||
|  * This can be considered as an ugly hack inside the Android universe. **/ | ||||
| public class ActivityCommunicator { | ||||
|  | ||||
|     private static ActivityCommunicator activityCommunicator = null; | ||||
| @@ -39,4 +41,9 @@ public class ActivityCommunicator { | ||||
|  | ||||
|     // Thumbnail send from VideoItemDetailFragment to BackgroundPlayer | ||||
|     public volatile Bitmap backgroundPlayerThumbnail; | ||||
|  | ||||
|     // Sent from any activity to ErrorActivity. | ||||
|     public volatile List<Exception> errorList; | ||||
|     public volatile Class returnActivity; | ||||
|     public volatile ErrorActivity.ErrorInfo errorInfo; | ||||
| } | ||||
|   | ||||
| @@ -1,18 +1,13 @@ | ||||
| package org.schabi.newpipe; | ||||
|  | ||||
| import android.Manifest; | ||||
| import android.app.Activity; | ||||
| import android.app.Dialog; | ||||
| import android.app.DownloadManager; | ||||
| import android.app.Notification; | ||||
| import android.content.Context; | ||||
| import android.content.DialogInterface; | ||||
| import android.content.SharedPreferences; | ||||
| import android.content.pm.PackageManager; | ||||
| import android.net.Uri; | ||||
| import android.os.Bundle; | ||||
| import android.os.Environment; | ||||
| import android.preference.PreferenceManager; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.v4.app.ActivityCompat; | ||||
| import android.support.v4.app.DialogFragment; | ||||
| @@ -63,30 +58,92 @@ public class DownloadDialog extends DialogFragment { | ||||
|         if(ContextCompat.checkSelfPermission(this.getContext(),Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED) | ||||
|             ActivityCompat.requestPermissions(getActivity(),new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},0); | ||||
|         AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); | ||||
|         builder.setTitle(R.string.download_dialog_title) | ||||
|                 .setItems(R.array.download_options, new DialogInterface.OnClickListener() { | ||||
|         builder.setTitle(R.string.download_dialog_title); | ||||
|  | ||||
|         // If no audio stream available | ||||
|         if(arguments.getString(AUDIO_URL) == null) { | ||||
|             builder.setItems(R.array.download_options_no_audio, new DialogInterface.OnClickListener() { | ||||
|                 @Override | ||||
|                 public void onClick(DialogInterface dialog, int which) { | ||||
|                     Context context = getActivity(); | ||||
|                         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); | ||||
|                         String suffix = ""; | ||||
|                     String title = arguments.getString(TITLE); | ||||
|                         String url = ""; | ||||
|                         File downloadDir = NewPipeSettings.getDownloadFolder(); | ||||
|                     switch (which) { | ||||
|                         case 0:     // Video | ||||
|                                 suffix = arguments.getString(FILE_SUFFIX_VIDEO); | ||||
|                                 url = arguments.getString(VIDEO_URL); | ||||
|                                 downloadDir = NewPipeSettings.getVideoDownloadFolder(context); | ||||
|                                 break; | ||||
|                             case 1: | ||||
|                                 suffix = arguments.getString(FILE_SUFFIX_AUDIO); | ||||
|                                 url = arguments.getString(AUDIO_URL); | ||||
|                                 downloadDir = NewPipeSettings.getAudioDownloadFolder(context); | ||||
|                             download(arguments.getString(VIDEO_URL), | ||||
|                                     title, | ||||
|                                     arguments.getString(FILE_SUFFIX_VIDEO), context); | ||||
|                             break; | ||||
|                         default: | ||||
|                             Log.d(TAG, "lolz"); | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
|             // If no video stream available | ||||
|         } else if(arguments.getString(VIDEO_URL) == null) { | ||||
|             builder.setItems(R.array.download_options_no_video, new DialogInterface.OnClickListener() { | ||||
|                 @Override | ||||
|                 public void onClick(DialogInterface dialog, int which) { | ||||
|                     Context context = getActivity(); | ||||
|                     String title = arguments.getString(TITLE); | ||||
|                     switch (which) { | ||||
|                         case 0:     // Audio | ||||
|                             download(arguments.getString(AUDIO_URL), | ||||
|                                     title, | ||||
|                                     arguments.getString(FILE_SUFFIX_AUDIO), context); | ||||
|                             break; | ||||
|                         default: | ||||
|                             Log.d(TAG, "lolz"); | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
|             //if both streams ar available | ||||
|         } else { | ||||
|             builder.setItems(R.array.download_options, new DialogInterface.OnClickListener() { | ||||
|                 @Override | ||||
|                 public void onClick(DialogInterface dialog, int which) { | ||||
|                     Context context = getActivity(); | ||||
|                     String title = arguments.getString(TITLE); | ||||
|                     switch (which) { | ||||
|                         case 0:     // Video | ||||
|                             download(arguments.getString(VIDEO_URL), | ||||
|                                     title, | ||||
|                                     arguments.getString(FILE_SUFFIX_VIDEO), context); | ||||
|                             break; | ||||
|                         case 1: | ||||
|                             download(arguments.getString(AUDIO_URL), | ||||
|                                     title, | ||||
|                                     arguments.getString(FILE_SUFFIX_AUDIO), context); | ||||
|                             break; | ||||
|                         default: | ||||
|                             Log.d(TAG, "lolz"); | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|         return builder.create(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * #143 #44 #42 #22: make shure that the filename does not contain illegal chars. | ||||
|      * This should fix some of the "cannot download" problems. | ||||
|      * */ | ||||
|     private String createFileName(String fName) { | ||||
|         // from http://eng-przemelek.blogspot.de/2009/07/how-to-create-valid-file-name.html | ||||
|  | ||||
|         List<String> forbiddenCharsPatterns = new ArrayList<> (); | ||||
|         forbiddenCharsPatterns.add("[:]+"); // Mac OS, but it looks that also Windows XP | ||||
|         forbiddenCharsPatterns.add("[\\*\"/\\\\\\[\\]\\:\\;\\|\\=\\,]+");  // Windows | ||||
|         forbiddenCharsPatterns.add("[^\\w\\d\\.]+");  // last chance... only latin letters and digits | ||||
|         String nameToTest = fName; | ||||
|         for (String pattern : forbiddenCharsPatterns) { | ||||
|             nameToTest = nameToTest.replaceAll(pattern, "_"); | ||||
|         } | ||||
|         return nameToTest; | ||||
|     } | ||||
|  | ||||
|     private void download(String url, String title, String fileSuffix, Context context) { | ||||
|         File downloadDir = NewPipeSettings.getDownloadFolder(); | ||||
|  | ||||
|         if(!downloadDir.exists()) { | ||||
|             //attempt to create directory | ||||
|             boolean mkdir = downloadDir.mkdirs(); | ||||
| @@ -102,7 +159,7 @@ public class DownloadDialog extends DialogFragment { | ||||
|             Toast.makeText(context,message , Toast.LENGTH_LONG).show(); | ||||
|         } | ||||
|  | ||||
|                         File saveFilePath = new File(downloadDir,createFileName(title) + suffix); | ||||
|         File saveFilePath = new File(downloadDir,createFileName(title) + fileSuffix); | ||||
|  | ||||
|         long id = 0; | ||||
|         if (App.isUsingTor()) { | ||||
| @@ -130,25 +187,4 @@ public class DownloadDialog extends DialogFragment { | ||||
|         Log.i(TAG,"Started downloading '" + url + | ||||
|                 "' => '" + saveFilePath + "' #" + id); | ||||
|     } | ||||
|                 }); | ||||
|         return builder.create(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * #143 #44 #42 #22: make shure that the filename does not contain illegal chars. | ||||
|      * This should fix some of the "cannot download" problems. | ||||
|      * */ | ||||
|     private String createFileName(String fName) { | ||||
|         // from http://eng-przemelek.blogspot.de/2009/07/how-to-create-valid-file-name.html | ||||
|  | ||||
|         List<String> forbiddenCharsPatterns = new ArrayList<String> (); | ||||
|         forbiddenCharsPatterns.add("[:]+"); // Mac OS, but it looks that also Windows XP | ||||
|         forbiddenCharsPatterns.add("[\\*\"/\\\\\\[\\]\\:\\;\\|\\=\\,]+");  // Windows | ||||
|         forbiddenCharsPatterns.add("[^\\w\\d\\.]+");  // last chance... only latin letters and digits | ||||
|         String nameToTest = fName; | ||||
|         for (String pattern : forbiddenCharsPatterns) { | ||||
|             nameToTest = nameToTest.replaceAll(pattern, "_"); | ||||
|         } | ||||
|         return nameToTest; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -30,7 +30,7 @@ import info.guardianproject.netcipher.NetCipher; | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| public class Downloader implements org.schabi.newpipe.crawler.Downloader { | ||||
| public class Downloader implements org.schabi.newpipe.extractor.Downloader { | ||||
|      | ||||
|     private static final String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0"; | ||||
|  | ||||
|   | ||||
							
								
								
									
										404
									
								
								app/src/main/java/org/schabi/newpipe/ErrorActivity.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,404 @@ | ||||
|  | ||||
|  | ||||
| package org.schabi.newpipe; | ||||
|  | ||||
| import android.app.Activity; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.graphics.Color; | ||||
| import android.net.Uri; | ||||
| import android.os.AsyncTask; | ||||
| import android.os.Build; | ||||
| import android.preference.PreferenceManager; | ||||
| import android.support.design.widget.Snackbar; | ||||
| import android.support.v4.app.NavUtils; | ||||
| import android.support.v7.app.AppCompatActivity; | ||||
| import android.os.Bundle; | ||||
| import android.os.Handler; | ||||
| import android.util.Log; | ||||
| import android.view.Menu; | ||||
| import android.view.MenuInflater; | ||||
| import android.view.MenuItem; | ||||
| import android.view.View; | ||||
| import android.widget.Button; | ||||
| import android.widget.EditText; | ||||
| import android.widget.TextView; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| import org.apache.commons.lang.exception.ExceptionUtils; | ||||
| import org.json.JSONArray; | ||||
| import org.json.JSONObject; | ||||
| import org.schabi.newpipe.extractor.Parser; | ||||
|  | ||||
| import java.text.SimpleDateFormat; | ||||
| import java.util.Date; | ||||
| import java.util.List; | ||||
| import java.util.TimeZone; | ||||
| import java.util.Vector; | ||||
|  | ||||
| /** | ||||
|  * Created by Christian Schabesberger on 24.10.15. | ||||
|  * <p> | ||||
|  * Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org> | ||||
|  * ErrorActivity.java is part of NewPipe. | ||||
|  * <p> | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * <p> | ||||
|  * NewPipe is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU General Public License for more details. | ||||
|  * <p> | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| public class ErrorActivity extends AppCompatActivity { | ||||
|     public static class ErrorInfo { | ||||
|         public int userAction; | ||||
|         public String request; | ||||
|         public String serviceName; | ||||
|         public int message; | ||||
|  | ||||
|         public static ErrorInfo make(int userAction, String serviceName, String request, int message) { | ||||
|             ErrorInfo info = new ErrorInfo(); | ||||
|             info.userAction = userAction; | ||||
|             info.serviceName = serviceName; | ||||
|             info.request = request; | ||||
|             info.message = message; | ||||
|             return info; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static final String TAG = ErrorActivity.class.toString(); | ||||
|     public static final int SEARCHED = 0; | ||||
|     public static final int REQUESTED_STREAM = 1; | ||||
|     public static final int GET_SUGGESTIONS = 2; | ||||
|     public static final int SOMETHING_ELSE = 3; | ||||
|     public static final int USER_REPORT = 4; | ||||
|     public static final String SEARCHED_STRING = "searched"; | ||||
|     public static final String REQUESTED_STREAM_STRING = "requested stream"; | ||||
|     public static final String GET_SUGGESTIONS_STRING = "get suggestions"; | ||||
|     public static final String SOMETHING_ELSE_STRING = "something"; | ||||
|     public static final String USER_REPORT_STRING = "user report"; | ||||
|  | ||||
|     public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org"; | ||||
|     public static final String ERROR_EMAIL_SUBJECT = "Exception in NewPipe " + BuildConfig.VERSION_NAME; | ||||
|  | ||||
|     private List<Exception> errorList; | ||||
|     private ErrorInfo errorInfo; | ||||
|     private Class returnActivity; | ||||
|     private String currentTimeStamp; | ||||
|     private String globIpRange; | ||||
|     Thread globIpRangeThread = null; | ||||
|  | ||||
|     // views | ||||
|     private TextView errorView; | ||||
|     private EditText userCommentBox; | ||||
|     private Button reportButton; | ||||
|     private TextView infoView; | ||||
|     private TextView errorMessageView; | ||||
|  | ||||
|     public static void reportError(final Context context, final List<Exception> el, | ||||
|                                    final Class returnAcitivty, View rootView, final ErrorInfo errorInfo) { | ||||
|  | ||||
|         if (rootView != null) { | ||||
|             Snackbar.make(rootView, R.string.error_snackbar_message, Snackbar.LENGTH_LONG) | ||||
|                     .setActionTextColor(Color.YELLOW) | ||||
|                     .setAction(R.string.error_snackbar_action, new View.OnClickListener() { | ||||
|                         @Override | ||||
|                         public void onClick(View v) { | ||||
|                             ActivityCommunicator ac = ActivityCommunicator.getCommunicator(); | ||||
|                             ac.errorList = el; | ||||
|                             ac.returnActivity = returnAcitivty; | ||||
|                             ac.errorInfo = errorInfo; | ||||
|                             Intent intent = new Intent(context, ErrorActivity.class); | ||||
|                             context.startActivity(intent); | ||||
|                         } | ||||
|                     }).show(); | ||||
|         } else { | ||||
|             ActivityCommunicator ac = ActivityCommunicator.getCommunicator(); | ||||
|             ac.errorList = el; | ||||
|             ac.returnActivity = returnAcitivty; | ||||
|             ac.errorInfo = errorInfo; | ||||
|             Intent intent = new Intent(context, ErrorActivity.class); | ||||
|             context.startActivity(intent); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static void reportError(final Context context, final Exception e, | ||||
|                                    final Class returnAcitivty, View rootView, final ErrorInfo errorInfo) { | ||||
|         List<Exception> el = null; | ||||
|         if(e != null) { | ||||
|             el = new Vector<>(); | ||||
|             el.add(e); | ||||
|         } | ||||
|         reportError(context, el, returnAcitivty, rootView, errorInfo); | ||||
|     } | ||||
|  | ||||
|     // async call | ||||
|     public static void reportError(Handler handler, final Context context, final Exception e, | ||||
|                                    final Class returnAcitivty, final View rootView, final ErrorInfo errorInfo) { | ||||
|  | ||||
|         List<Exception> el = null; | ||||
|         if(e != null) { | ||||
|             el = new Vector<>(); | ||||
|             el.add(e); | ||||
|         } | ||||
|         reportError(handler, context, el, returnAcitivty, rootView, errorInfo); | ||||
|     } | ||||
|  | ||||
|     // async call | ||||
|     public static void reportError(Handler handler, final Context context, final List<Exception> el, | ||||
|                                    final Class returnAcitivty, final View rootView, final ErrorInfo errorInfo) { | ||||
|         handler.post(new Runnable() { | ||||
|             @Override | ||||
|             public void run() { | ||||
|                 reportError(context, el, returnAcitivty, rootView, errorInfo); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void onCreate(Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         setContentView(R.layout.activity_error); | ||||
|         getSupportActionBar().setDisplayHomeAsUpEnabled(true); | ||||
|  | ||||
|         ActivityCommunicator ac = ActivityCommunicator.getCommunicator(); | ||||
|         errorList = ac.errorList; | ||||
|         returnActivity = ac.returnActivity; | ||||
|         errorInfo = ac.errorInfo; | ||||
|  | ||||
|         reportButton = (Button) findViewById(R.id.errorReportButton); | ||||
|         userCommentBox = (EditText) findViewById(R.id.errorCommentBox); | ||||
|         errorView = (TextView) findViewById(R.id.errorView); | ||||
|         infoView = (TextView) findViewById(R.id.errorInfosView); | ||||
|         errorMessageView = (TextView) findViewById(R.id.errorMessageView); | ||||
|  | ||||
|         errorView.setText(formErrorText(errorList)); | ||||
|  | ||||
|         //importand add gurumeditaion | ||||
|         addGuruMeditaion(); | ||||
|         currentTimeStamp = getCurrentTimeStamp(); | ||||
|         buildInfo(errorInfo); | ||||
|  | ||||
|         reportButton.setOnClickListener(new View.OnClickListener() { | ||||
|             @Override | ||||
|             public void onClick(View v) { | ||||
|  | ||||
|                 Intent intent = new Intent(Intent.ACTION_SENDTO); | ||||
|                 intent.setData(Uri.parse("mailto:" + ERROR_EMAIL_ADDRESS)) | ||||
|                         .putExtra(Intent.EXTRA_SUBJECT, ERROR_EMAIL_SUBJECT) | ||||
|                         .putExtra(Intent.EXTRA_TEXT, buildJson()); | ||||
|  | ||||
|                 startActivity(Intent.createChooser(intent, "Send Email")); | ||||
|             } | ||||
|         }); | ||||
|         reportButton.setEnabled(false); | ||||
|  | ||||
|         globIpRangeThread = new Thread(new IpRagneRequester()); | ||||
|         globIpRangeThread.start(); | ||||
|  | ||||
|         if(errorInfo.message != 0) { | ||||
|             errorMessageView.setText(errorInfo.message); | ||||
|         } else { | ||||
|             errorMessageView.setVisibility(View.GONE); | ||||
|             findViewById(R.id.messageWhatHappenedView).setVisibility(View.GONE); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean onCreateOptionsMenu(Menu menu) { | ||||
|         MenuInflater inflater = getMenuInflater(); | ||||
|         inflater.inflate(R.menu.error_menu, menu); | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean onOptionsItemSelected(MenuItem item) { | ||||
|         int id = item.getItemId(); | ||||
|         switch (id) { | ||||
|             case android.R.id.home: | ||||
|                 goToReturnActivity(); | ||||
|                 break; | ||||
|             case R.id.menu_item_share_error: { | ||||
|                 Intent intent = new Intent(); | ||||
|                 intent.setAction(Intent.ACTION_SEND); | ||||
|                 intent.putExtra(Intent.EXTRA_TEXT, buildJson()); | ||||
|                 intent.setType("text/plain"); | ||||
|                 startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title))); | ||||
|             } | ||||
|             break; | ||||
|         } | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     private String formErrorText(List<Exception> el) { | ||||
|         String text = ""; | ||||
|         if(el != null) { | ||||
|             for (Exception e : el) { | ||||
|                 text += "-------------------------------------\n" | ||||
|                         + ExceptionUtils.getStackTrace(e); | ||||
|             } | ||||
|         } | ||||
|         text += "-------------------------------------"; | ||||
|         return text; | ||||
|     } | ||||
|  | ||||
|     private void goToReturnActivity() { | ||||
|         if (returnActivity == null) { | ||||
|             super.onBackPressed(); | ||||
|         } else { | ||||
|             Intent intent; | ||||
|             if (returnActivity != null && | ||||
|                     returnActivity.isAssignableFrom(Activity.class)) { | ||||
|                 intent = new Intent(this, returnActivity); | ||||
|             } else { | ||||
|                 intent = new Intent(this, VideoItemListActivity.class); | ||||
|             } | ||||
|             intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); | ||||
|             NavUtils.navigateUpTo(this, intent); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void buildInfo(ErrorInfo info) { | ||||
|         TextView infoLabelView = (TextView) findViewById(R.id.errorInfoLabelsView); | ||||
|         TextView infoView = (TextView) findViewById(R.id.errorInfosView); | ||||
|         String text = ""; | ||||
|  | ||||
|         infoLabelView.setText(getString(R.string.info_labels).replace("\\n", "\n")); | ||||
|  | ||||
|         text += getUserActionString(info.userAction) | ||||
|                 + "\n" + info.request | ||||
|                 + "\n" + getContentLangString() | ||||
|                 + "\n" + info.serviceName | ||||
|                 + "\n" + currentTimeStamp | ||||
|                 + "\n" + BuildConfig.VERSION_NAME | ||||
|                 + "\n" + getOsString(); | ||||
|  | ||||
|         infoView.setText(text); | ||||
|     } | ||||
|  | ||||
|     private String buildJson() { | ||||
|         JSONObject errorObject = new JSONObject(); | ||||
|  | ||||
|         try { | ||||
|             errorObject.put("user_action", getUserActionString(errorInfo.userAction)) | ||||
|                     .put("request", errorInfo.request) | ||||
|                     .put("content_language", getContentLangString()) | ||||
|                     .put("service", errorInfo.serviceName) | ||||
|                     .put("version", BuildConfig.VERSION_NAME) | ||||
|                     .put("os", getOsString()) | ||||
|                     .put("time", currentTimeStamp) | ||||
|                     .put("ip_range", globIpRange); | ||||
|  | ||||
|             JSONArray exceptionArray = new JSONArray(); | ||||
|             if(errorList != null) { | ||||
|                 for (Exception e : errorList) { | ||||
|                     exceptionArray.put(ExceptionUtils.getStackTrace(e)); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             errorObject.put("exceptions", exceptionArray); | ||||
|             errorObject.put("user_comment", userCommentBox.getText().toString()); | ||||
|  | ||||
|             return errorObject.toString(3); | ||||
|         } catch (Exception e) { | ||||
|             Log.e(TAG, "Error while erroring: Could not build json"); | ||||
|             e.printStackTrace(); | ||||
|         } | ||||
|  | ||||
|         return ""; | ||||
|     } | ||||
|  | ||||
|     private String getUserActionString(int userAction) { | ||||
|         switch (userAction) { | ||||
|             case REQUESTED_STREAM: | ||||
|                 return REQUESTED_STREAM_STRING; | ||||
|             case SEARCHED: | ||||
|                 return SEARCHED_STRING; | ||||
|             case GET_SUGGESTIONS: | ||||
|                 return GET_SUGGESTIONS_STRING; | ||||
|             case SOMETHING_ELSE: | ||||
|                 return SOMETHING_ELSE_STRING; | ||||
|             case USER_REPORT: | ||||
|                 return USER_REPORT_STRING; | ||||
|             default: | ||||
|                 return "Your description is in another castle."; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private String getContentLangString() { | ||||
|         return PreferenceManager.getDefaultSharedPreferences(this) | ||||
|                 .getString(this.getString(R.string.search_language_key), "none"); | ||||
|     } | ||||
|  | ||||
|     private String getOsString() { | ||||
|         String osBase = Build.VERSION.SDK_INT >= 23 ? Build.VERSION.BASE_OS : "Android"; | ||||
|         return System.getProperty("os.name") | ||||
|                 + " " + (osBase.isEmpty() ? "Android" : osBase) | ||||
|                 + " " + Build.VERSION.RELEASE | ||||
|                 + " - " + Integer.toString(Build.VERSION.SDK_INT); | ||||
|     } | ||||
|  | ||||
|     private void addGuruMeditaion() { | ||||
|         //just an easter egg | ||||
|         TextView sorryView = (TextView) findViewById(R.id.errorSorryView); | ||||
|         String text = sorryView.getText().toString(); | ||||
|         text += "\n" + getString(R.string.guru_meditation); | ||||
|         sorryView.setText(text); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onBackPressed() { | ||||
|         //super.onBackPressed(); | ||||
|         goToReturnActivity(); | ||||
|     } | ||||
|  | ||||
|     public String getCurrentTimeStamp() { | ||||
|         SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm"); | ||||
|         df.setTimeZone(TimeZone.getTimeZone("GMT")); | ||||
|         return df.format(new Date()); | ||||
|     } | ||||
|  | ||||
|     private class IpRagneRequester implements Runnable { | ||||
|         Handler h = new Handler(); | ||||
|         public void run() { | ||||
|             String ipRange = "none"; | ||||
|             try { | ||||
|                 Downloader dl = new Downloader(); | ||||
|                 String ip = dl.download("https://ifcfg.me/ip"); | ||||
|  | ||||
|                 ipRange = Parser.matchGroup1("([0-9]*\\.[0-9]*\\.)[0-9]*\\.[0-9]*", ip) | ||||
|                         + "0.0"; | ||||
|             } catch(Exception e) { | ||||
|                 Log.d(TAG, "Error while error: could not get iprange"); | ||||
|                 e.printStackTrace(); | ||||
|             } finally { | ||||
|                 h.post(new IpRageReturnRunnable(ipRange)); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|     private class IpRageReturnRunnable implements Runnable { | ||||
|         String ipRange; | ||||
|         public IpRageReturnRunnable(String ipRange) { | ||||
|             this.ipRange = ipRange; | ||||
|         } | ||||
|         public void run() { | ||||
|             globIpRange = ipRange; | ||||
|             if(infoView != null) { | ||||
|                 String text = infoView.getText().toString(); | ||||
|                 text += "\n" + globIpRange; | ||||
|                 infoView.setText(text); | ||||
|                 reportButton.setEnabled(true); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -34,6 +34,9 @@ import java.util.Locale; | ||||
|  | ||||
| public class Localization { | ||||
|  | ||||
|     private Localization() { | ||||
|     } | ||||
|  | ||||
|     public static Locale getPreferredLocale(Context context) { | ||||
|         SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); | ||||
|  | ||||
|   | ||||
| @@ -33,6 +33,10 @@ import java.io.File; | ||||
|  * Helper for global settings | ||||
|  */ | ||||
| public class NewPipeSettings { | ||||
|  | ||||
|     private NewPipeSettings() { | ||||
|     } | ||||
|  | ||||
|     public static void initSettings(Context context) { | ||||
|         PreferenceManager.setDefaultValues(context, R.xml.settings, false); | ||||
|         getVideoDownloadFolder(context); | ||||
|   | ||||
| @@ -119,6 +119,7 @@ public class SettingsActivity extends PreferenceActivity  { | ||||
|                 public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, | ||||
|                                                       String key) { | ||||
|                     Activity a = getActivity(); | ||||
|                     if(a != null) { | ||||
|                         updateSummary(); | ||||
|  | ||||
|                         if (defaultPreferences.getBoolean(USE_TOR_KEY, false)) { | ||||
| @@ -133,6 +134,7 @@ public class SettingsActivity extends PreferenceActivity  { | ||||
|                             App.configureTor(false); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             }; | ||||
|             defaultPreferences.registerOnSharedPreferenceChangeListener(prefListener); | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,82 @@ | ||||
| package org.schabi.newpipe; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.database.Cursor; | ||||
| import android.database.MatrixCursor; | ||||
| import android.support.v4.widget.CursorAdapter; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.TextView; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
|  | ||||
| /** | ||||
|  * Created by Madiyar on 23.02.2016. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org> | ||||
|  * SuggestionListAdapter.java is part of NewPipe. | ||||
|  * | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * NewPipe is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| public class SuggestionListAdapter extends CursorAdapter { | ||||
|  | ||||
|     private String[] columns = new String[]{"_id", "title"}; | ||||
|  | ||||
|     public SuggestionListAdapter(Context context) { | ||||
|         super(context, null, false); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public View newView(Context context, Cursor cursor, ViewGroup parent) { | ||||
|         ViewHolder viewHolder; | ||||
|  | ||||
|         View view = LayoutInflater.from(context).inflate(android.R.layout.simple_list_item_1, parent, false); | ||||
|         viewHolder = new ViewHolder(); | ||||
|         viewHolder.suggestionTitle = (TextView) view.findViewById(android.R.id.text1); | ||||
|         view.setTag(viewHolder); | ||||
|  | ||||
|  | ||||
|         return view; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void bindView(View view, Context context, Cursor cursor) { | ||||
|         ViewHolder viewHolder = (ViewHolder) view.getTag(); | ||||
|         viewHolder.suggestionTitle.setText(cursor.getString(1)); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     public void updateAdapter(ArrayList<String> suggestions) { | ||||
|         MatrixCursor cursor = new MatrixCursor(columns); | ||||
|         int i = 0; | ||||
|         for (String s : suggestions) { | ||||
|             String[] temp = new String[2]; | ||||
|             temp[0] = Integer.toString(i); | ||||
|             temp[1] = s; | ||||
|             i++; | ||||
|             cursor.addRow(temp); | ||||
|         } | ||||
|         changeCursor(cursor); | ||||
|     } | ||||
|  | ||||
|     public String getSuggestion(int position) { | ||||
|         return ((Cursor) getItem(position)).getString(1); | ||||
|     } | ||||
|  | ||||
|     private class ViewHolder { | ||||
|         public TextView suggestionTitle; | ||||
|     } | ||||
| } | ||||
| @@ -1,13 +1,14 @@ | ||||
| package org.schabi.newpipe; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.ImageView; | ||||
| import android.widget.TextView; | ||||
|  | ||||
| import org.schabi.newpipe.crawler.VideoPreviewInfo; | ||||
| import org.schabi.newpipe.extractor.AbstractVideoInfo; | ||||
| import org.schabi.newpipe.extractor.StreamPreviewInfo; | ||||
|  | ||||
| import com.nostra13.universalimageloader.core.DisplayImageOptions; | ||||
| import com.nostra13.universalimageloader.core.ImageLoader; | ||||
|  | ||||
| @@ -31,7 +32,7 @@ import com.nostra13.universalimageloader.core.ImageLoader; | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| class VideoInfoItemViewCreator { | ||||
| public class VideoInfoItemViewCreator { | ||||
|     private final LayoutInflater inflater; | ||||
|     private ImageLoader imageLoader = ImageLoader.getInstance(); | ||||
|     private DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder().cacheInMemory(true).build(); | ||||
| @@ -40,8 +41,10 @@ class VideoInfoItemViewCreator { | ||||
|         this.inflater = inflater; | ||||
|     } | ||||
|  | ||||
|     public View getViewFromVideoInfoItem(View convertView, ViewGroup parent, VideoPreviewInfo info, Context context) { | ||||
|     public View getViewFromVideoInfoItem(View convertView, ViewGroup parent, StreamPreviewInfo info) { | ||||
|         ViewHolder holder; | ||||
|  | ||||
|         // generate holder | ||||
|         if(convertView == null) { | ||||
|             convertView = inflater.inflate(R.layout.video_item, parent, false); | ||||
|             holder = new ViewHolder(); | ||||
| @@ -56,20 +59,43 @@ class VideoInfoItemViewCreator { | ||||
|             holder = (ViewHolder) convertView.getTag(); | ||||
|         } | ||||
|  | ||||
|         // fill with information | ||||
|  | ||||
|         /* | ||||
|         if(info.thumbnail == null) { | ||||
|             holder.itemThumbnailView.setImageResource(R.drawable.dummy_thumbnail); | ||||
|         } else { | ||||
|             holder.itemThumbnailView.setImageBitmap(info.thumbnail); | ||||
|         } | ||||
|         */ | ||||
|         holder.itemVideoTitleView.setText(info.title); | ||||
|         if(info.uploader != null && !info.uploader.isEmpty()) { | ||||
|             holder.itemUploaderView.setText(info.uploader); | ||||
|         holder.itemDurationView.setText(info.duration); | ||||
|         } else { | ||||
|             holder.itemDurationView.setVisibility(View.INVISIBLE); | ||||
|         } | ||||
|         if(info.duration > 0) { | ||||
|             holder.itemDurationView.setText(getDurationString(info.duration)); | ||||
|         } else { | ||||
|             if(info.stream_type == AbstractVideoInfo.StreamType.LIVE_STREAM) { | ||||
|                 holder.itemDurationView.setText(R.string.duration_live); | ||||
|             } else { | ||||
|                 holder.itemDurationView.setVisibility(View.GONE); | ||||
|             } | ||||
|         } | ||||
|         if(info.view_count >= 0) { | ||||
|             holder.itemViewCountView.setText(shortViewCount(info.view_count)); | ||||
|         } else { | ||||
|             holder.itemViewCountView.setVisibility(View.GONE); | ||||
|         } | ||||
|         if(!info.upload_date.isEmpty()) { | ||||
|             holder.itemUploadDateView.setText(info.upload_date + " • "); | ||||
|         } | ||||
|  | ||||
|         holder.itemThumbnailView.setImageResource(R.drawable.dummy_thumbnail); | ||||
|         if(info.thumbnail_url != null && !info.thumbnail_url.isEmpty()) { | ||||
|             imageLoader.displayImage(info.thumbnail_url, holder.itemThumbnailView, displayImageOptions); | ||||
|         } | ||||
|  | ||||
|         return convertView; | ||||
|     } | ||||
| @@ -79,16 +105,69 @@ class VideoInfoItemViewCreator { | ||||
|         public TextView itemVideoTitleView, itemUploaderView, itemDurationView, itemUploadDateView, itemViewCountView; | ||||
|     } | ||||
|  | ||||
|     private String shortViewCount(Long view_count){ | ||||
|         if(view_count >= 1000000000){ | ||||
|             return Long.toString(view_count/1000000000)+"B views"; | ||||
|         }else if(view_count>=1000000){ | ||||
|             return Long.toString(view_count/1000000)+"M views"; | ||||
|         }else if(view_count>=1000){ | ||||
|             return Long.toString(view_count/1000)+"K views"; | ||||
|     private String shortViewCount(Long viewCount){ | ||||
|         if(viewCount >= 1000000000){ | ||||
|             return Long.toString(viewCount/1000000000)+"B views"; | ||||
|         }else if(viewCount>=1000000){ | ||||
|             return Long.toString(viewCount/1000000)+"M views"; | ||||
|         }else if(viewCount>=1000){ | ||||
|             return Long.toString(viewCount/1000)+"K views"; | ||||
|         }else { | ||||
|             return Long.toString(view_count)+" views"; | ||||
|             return Long.toString(viewCount)+" views"; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static String getDurationString(int duration) { | ||||
|         String output = ""; | ||||
|         int days = duration / (24 * 60 * 60); /* greater than a day */ | ||||
|         duration %= (24 * 60 * 60); | ||||
|         int hours = duration / (60 * 60); /* greater than an hour */ | ||||
|         duration %= (60 * 60); | ||||
|         int minutes = duration / 60; | ||||
|         int seconds = duration % 60; | ||||
|  | ||||
|         //handle days | ||||
|         if(days > 0) { | ||||
|             output = Integer.toString(days) + ":"; | ||||
|         } | ||||
|         // handle hours | ||||
|         if(hours > 0 || !output.isEmpty()) { | ||||
|             if(hours > 0) { | ||||
|                 if(hours >= 10 || output.isEmpty()) { | ||||
|                     output += Integer.toString(minutes); | ||||
|                 } else { | ||||
|                     output += "0" + Integer.toString(minutes); | ||||
|                 } | ||||
|             } else { | ||||
|                 output += "00"; | ||||
|             } | ||||
|             output += ":"; | ||||
|         } | ||||
|         //handle minutes | ||||
|         if(minutes > 0 || !output.isEmpty()) { | ||||
|             if(minutes > 0) { | ||||
|                 if(minutes >= 10 || output.isEmpty()) { | ||||
|                     output += Integer.toString(minutes); | ||||
|                 } else { | ||||
|                     output += "0" + Integer.toString(minutes); | ||||
|                 } | ||||
|             } else { | ||||
|                 output += "00"; | ||||
|             } | ||||
|             output += ":"; | ||||
|         } | ||||
|  | ||||
|         //handle seconds | ||||
|         if(output.isEmpty()) { | ||||
|             output += "0:"; | ||||
|         } | ||||
|  | ||||
|         if(seconds >= 10) { | ||||
|             output += Integer.toString(seconds); | ||||
|         } else { | ||||
|             output += "0" + Integer.toString(seconds); | ||||
|         } | ||||
|  | ||||
|         return output; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -11,8 +11,8 @@ import android.view.Menu; | ||||
| import android.view.MenuItem; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| import org.schabi.newpipe.crawler.ServiceList; | ||||
| import org.schabi.newpipe.crawler.StreamingService; | ||||
| import org.schabi.newpipe.extractor.ServiceList; | ||||
| import org.schabi.newpipe.extractor.StreamingService; | ||||
|  | ||||
|  | ||||
| /** | ||||
| @@ -73,7 +73,7 @@ public class VideoItemDetailActivity extends AppCompatActivity { | ||||
|                 StreamingService[] serviceList = ServiceList.getServices(); | ||||
|                 //StreamExtractor videoExtractor = null; | ||||
|                 for (int i = 0; i < serviceList.length; i++) { | ||||
|                     if (serviceList[i].getUrlIdHandler().acceptUrl(videoUrl)) { | ||||
|                     if (serviceList[i].getUrlIdHandlerInstance().acceptUrl(videoUrl)) { | ||||
|                         arguments.putInt(VideoItemDetailFragment.STREAMING_SERVICE, i); | ||||
|                         currentStreamingService = i; | ||||
|                         //videoExtractor = ServiceList.getService(i).getExtractorInstance(); | ||||
|   | ||||
| @@ -38,6 +38,7 @@ import android.widget.Toast; | ||||
|  | ||||
| import java.io.IOException; | ||||
|  | ||||
| import com.google.android.exoplayer.util.Util; | ||||
| import com.nostra13.universalimageloader.core.DisplayImageOptions; | ||||
| import com.nostra13.universalimageloader.core.ImageLoader; | ||||
| import com.nostra13.universalimageloader.core.assist.FailReason; | ||||
| @@ -46,14 +47,20 @@ import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Vector; | ||||
|  | ||||
| import org.schabi.newpipe.crawler.MediaFormat; | ||||
| import org.schabi.newpipe.crawler.ParsingException; | ||||
| import org.schabi.newpipe.crawler.ServiceList; | ||||
| import org.schabi.newpipe.crawler.StreamExtractor; | ||||
| import org.schabi.newpipe.crawler.VideoPreviewInfo; | ||||
| import org.schabi.newpipe.crawler.StreamingService; | ||||
| import org.schabi.newpipe.crawler.VideoInfo; | ||||
| import org.schabi.newpipe.crawler.services.youtube.YoutubeStreamExtractor; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.AudioStream; | ||||
| import org.schabi.newpipe.extractor.MediaFormat; | ||||
| import org.schabi.newpipe.extractor.ParsingException; | ||||
| import org.schabi.newpipe.extractor.ServiceList; | ||||
| import org.schabi.newpipe.extractor.StreamExtractor; | ||||
| import org.schabi.newpipe.extractor.StreamInfo; | ||||
| import org.schabi.newpipe.extractor.StreamPreviewInfo; | ||||
| import org.schabi.newpipe.extractor.StreamingService; | ||||
| import org.schabi.newpipe.extractor.VideoStream; | ||||
| import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor; | ||||
| import org.schabi.newpipe.player.BackgroundPlayer; | ||||
| import org.schabi.newpipe.player.PlayVideoActivity; | ||||
| import org.schabi.newpipe.player.ExoPlayerActivity; | ||||
|  | ||||
|  | ||||
| /** | ||||
| @@ -98,6 +105,7 @@ public class VideoItemDetailFragment extends Fragment { | ||||
|     private Bitmap videoThumbnail; | ||||
|  | ||||
|     private View thumbnailWindowLayout; | ||||
|     //this only remains due to downwards compatibility | ||||
|     private FloatingActionButton playVideoButton; | ||||
|     private final Point initialThumbnailPos = new Point(0, 0); | ||||
|  | ||||
| @@ -126,11 +134,34 @@ public class VideoItemDetailFragment extends Fragment { | ||||
|  | ||||
|         @Override | ||||
|         public void run() { | ||||
|             StreamInfo streamInfo = null; | ||||
|             try { | ||||
|                 streamExtractor = service.getExtractorInstance(videoUrl, new Downloader()); | ||||
|                 VideoInfo videoInfo = VideoInfo.getVideoInfo(streamExtractor, new Downloader()); | ||||
|                 streamInfo = StreamInfo.getVideoInfo(streamExtractor, new Downloader()); | ||||
|  | ||||
|                 h.post(new VideoResultReturnedRunnable(videoInfo)); | ||||
|                 h.post(new VideoResultReturnedRunnable(streamInfo)); | ||||
|  | ||||
|                 // look for errors during extraction | ||||
|                 // this if statement only covers extra information. | ||||
|                 // if these are not available or caused an error, they are just not available | ||||
|                 // but don't render the stream information unusalbe. | ||||
|                 if(streamInfo != null && | ||||
|                         !streamInfo.errors.isEmpty()) { | ||||
|                     Log.e(TAG, "OCCURRED ERRORS DURING EXTRACTION:"); | ||||
|                     for (Exception e : streamInfo.errors) { | ||||
|                         e.printStackTrace(); | ||||
|                         Log.e(TAG, "------"); | ||||
|                     } | ||||
|  | ||||
|                     Activity a = getActivity(); | ||||
|                     View rootView = a != null ? a.findViewById(R.id.videoitem_detail) : null; | ||||
|                     ErrorActivity.reportError(h, getActivity(), | ||||
|                             streamInfo.errors, null, rootView, | ||||
|                             ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, | ||||
|                                     service.getServiceInfo().name, videoUrl, 0 /* no message for the user */)); | ||||
|                 } | ||||
|  | ||||
|                 // These errors render the stream information unusable. | ||||
|             } catch (IOException e) { | ||||
|                 postNewErrorToast(h, R.string.network_error); | ||||
|                 e.printStackTrace(); | ||||
| @@ -146,6 +177,13 @@ public class VideoItemDetailFragment extends Fragment { | ||||
|                         onErrorBlockedByGema(); | ||||
|                     } | ||||
|                 }); | ||||
|             } catch(YoutubeStreamExtractor.LiveStreamException e) { | ||||
|                 h.post(new Runnable() { | ||||
|                     @Override | ||||
|                     public void run() { | ||||
|                         onNotSpecifiedContentErrorWithMessage(R.string.live_streams_not_supported); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|             // ---------------------------------------- | ||||
|             catch(StreamExtractor.ContentNotAvailableException e) { | ||||
| @@ -156,26 +194,67 @@ public class VideoItemDetailFragment extends Fragment { | ||||
|                     } | ||||
|                 }); | ||||
|                 e.printStackTrace(); | ||||
|             } catch(StreamInfo.StreamExctractException e) { | ||||
|                 if(!streamInfo.errors.isEmpty()) { | ||||
|                     // !!! if this case ever kicks in someone gets kicked out !!! | ||||
|                     ErrorActivity.reportError(h, getActivity(), e, VideoItemListActivity.class, null, | ||||
|                             ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, | ||||
|                                     service.getServiceInfo().name, videoUrl, R.string.could_not_get_stream)); | ||||
|                 } else { | ||||
|                     ErrorActivity.reportError(h, getActivity(), streamInfo.errors, VideoItemListActivity.class, null, | ||||
|                             ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, | ||||
|                                     service.getServiceInfo().name, videoUrl, R.string.could_not_get_stream)); | ||||
|                 } | ||||
|                 h.post(new Runnable() { | ||||
|                     @Override | ||||
|                     public void run() { | ||||
|                         getActivity().finish(); | ||||
|                     } | ||||
|                 }); | ||||
|                 e.printStackTrace(); | ||||
|             } catch (ParsingException e) { | ||||
|                 postNewErrorToast(h, e.getMessage()); | ||||
|                 ErrorActivity.reportError(h, getActivity(), e, VideoItemListActivity.class, null, | ||||
|                         ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, | ||||
|                                 service.getServiceInfo().name, videoUrl, R.string.parsing_error)); | ||||
|                         h.post(new Runnable() { | ||||
|                             @Override | ||||
|                             public void run() { | ||||
|                                 getActivity().finish(); | ||||
|                             } | ||||
|                         }); | ||||
|                 e.printStackTrace(); | ||||
|             } catch(Exception e) { | ||||
|                 postNewErrorToast(h, R.string.general_error); | ||||
|                 ErrorActivity.reportError(h, getActivity(), e, VideoItemListActivity.class, null, | ||||
|                         ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, | ||||
|                                 service.getServiceInfo().name, videoUrl, R.string.general_error)); | ||||
|                         h.post(new Runnable() { | ||||
|                             @Override | ||||
|                             public void run() { | ||||
|                                 getActivity().finish(); | ||||
|                             } | ||||
|                         }); | ||||
|                 e.printStackTrace(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private class VideoResultReturnedRunnable implements Runnable { | ||||
|         private final VideoInfo videoInfo; | ||||
|         public VideoResultReturnedRunnable(VideoInfo videoInfo) { | ||||
|             this.videoInfo = videoInfo; | ||||
|         private final StreamInfo streamInfo; | ||||
|         public VideoResultReturnedRunnable(StreamInfo streamInfo) { | ||||
|             this.streamInfo = streamInfo; | ||||
|         } | ||||
|         @Override | ||||
|         public void run() { | ||||
|             //todo: fix expired thread error: | ||||
|             // If the thread calling this runnable is expired, the following function will crash. | ||||
|             updateInfo(videoInfo); | ||||
|             Activity a = getActivity(); | ||||
|             if(a != null) { | ||||
|                 boolean showAgeRestrictedContent = PreferenceManager.getDefaultSharedPreferences(a) | ||||
|                         .getBoolean(activity.getString(R.string.show_age_restricted_content), false); | ||||
|                 if (streamInfo.age_limit == 0 || showAgeRestrictedContent) { | ||||
|                     updateInfo(streamInfo); | ||||
|                 } else { | ||||
|                     onNotSpecifiedContentErrorWithMessage(R.string.video_is_age_restricted); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -185,8 +264,10 @@ public class VideoItemDetailFragment extends Fragment { | ||||
|  | ||||
|         @Override | ||||
|         public void onLoadingFailed(String imageUri, View view, FailReason failReason) { | ||||
|             if(getContext() != null) { | ||||
|                 Toast.makeText(VideoItemDetailFragment.this.getActivity(), | ||||
|                         R.string.could_not_load_thumbnails, Toast.LENGTH_LONG).show(); | ||||
|             } | ||||
|             failReason.getCause().printStackTrace(); | ||||
|         } | ||||
|  | ||||
| @@ -197,7 +278,7 @@ public class VideoItemDetailFragment extends Fragment { | ||||
|         public void onLoadingCancelled(String imageUri, View view) {} | ||||
|     } | ||||
|  | ||||
|     private void updateInfo(final VideoInfo info) { | ||||
|     private void updateInfo(final StreamInfo info) { | ||||
|         try { | ||||
|             Context c = getContext(); | ||||
|             VideoInfoItemViewCreator videoItemViewCreator = | ||||
| @@ -223,16 +304,31 @@ public class VideoItemDetailFragment extends Fragment { | ||||
|             Button backgroundButton = (Button) | ||||
|                     activity.findViewById(R.id.detailVideoThumbnailWindowBackgroundButton); | ||||
|             View topView = activity.findViewById(R.id.detailTopView); | ||||
|             View nextVideoView = videoItemViewCreator | ||||
|                     .getViewFromVideoInfoItem(null, nextVideoFrame, info.next_video, getContext()); | ||||
|             View nextVideoView = null; | ||||
|             if(info.next_video != null) { | ||||
|                 nextVideoView = videoItemViewCreator | ||||
|                         .getViewFromVideoInfoItem(null, nextVideoFrame, info.next_video); | ||||
|             } else { | ||||
|                 activity.findViewById(R.id.detailNextVidButtonAndContentLayout).setVisibility(View.GONE); | ||||
|                 activity.findViewById(R.id.detailNextVideoTitle).setVisibility(View.GONE); | ||||
|                 activity.findViewById(R.id.detailNextVideoButton).setVisibility(View.GONE); | ||||
|             } | ||||
|  | ||||
|             progressBar.setVisibility(View.GONE); | ||||
|             if(nextVideoView != null) { | ||||
|                 nextVideoFrame.addView(nextVideoView); | ||||
|             } | ||||
|  | ||||
|             initThumbnailViews(info, nextVideoFrame); | ||||
|  | ||||
|             textContentLayout.setVisibility(View.VISIBLE); | ||||
|             if (android.os.Build.VERSION.SDK_INT < 18) { | ||||
|                 playVideoButton.setVisibility(View.VISIBLE); | ||||
|             } else { | ||||
|                 ImageView playArrowView = (ImageView) activity.findViewById(R.id.playArrowView); | ||||
|                 playArrowView.setVisibility(View.VISIBLE); | ||||
|             } | ||||
|  | ||||
|             if (!showNextVideoItem) { | ||||
|                 nextVideoRootFrame.setVisibility(View.GONE); | ||||
|                 similarTitle.setVisibility(View.GONE); | ||||
| @@ -258,20 +354,49 @@ public class VideoItemDetailFragment extends Fragment { | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             uploaderView.setText(info.uploader); | ||||
|             // Since newpipe is designed to work even if certain information is not available, | ||||
|             // the UI has to react on missing information. | ||||
|             videoTitleView.setText(info.title); | ||||
|             if(!info.uploader.isEmpty()) { | ||||
|                 uploaderView.setText(info.uploader); | ||||
|             } else { | ||||
|                 activity.findViewById(R.id.detailUploaderWrapView).setVisibility(View.GONE); | ||||
|             } | ||||
|             if(info.view_count >= 0) { | ||||
|                 viewCountView.setText(Localization.localizeViewCount(info.view_count, c)); | ||||
|             thumbsUpView.setText(Localization.localizeNumber(info.like_count, c)); | ||||
|             } else { | ||||
|                 viewCountView.setVisibility(View.GONE); | ||||
|             } | ||||
|             if(info.dislike_count >= 0) { | ||||
|                 thumbsDownView.setText(Localization.localizeNumber(info.dislike_count, c)); | ||||
|             } else { | ||||
|                 thumbsDownView.setVisibility(View.INVISIBLE); | ||||
|                 activity.findViewById(R.id.detailThumbsDownImgView).setVisibility(View.GONE); | ||||
|             } | ||||
|             if(info.like_count >= 0) { | ||||
|                 thumbsUpView.setText(Localization.localizeNumber(info.like_count, c)); | ||||
|             } else { | ||||
|                 thumbsUpView.setVisibility(View.GONE); | ||||
|                 activity.findViewById(R.id.detailThumbsUpImgView).setVisibility(View.GONE); | ||||
|                 thumbsDownView.setVisibility(View.GONE); | ||||
|                 activity.findViewById(R.id.detailThumbsDownImgView).setVisibility(View.GONE); | ||||
|             } | ||||
|             if(!info.upload_date.isEmpty()) { | ||||
|                 uploadDateView.setText(Localization.localizeDate(info.upload_date, c)); | ||||
|             } else { | ||||
|                 uploadDateView.setVisibility(View.GONE); | ||||
|             } | ||||
|             if(!info.description.isEmpty()) { | ||||
|                 descriptionView.setText(Html.fromHtml(info.description)); | ||||
|             } else { | ||||
|                 descriptionView.setVisibility(View.GONE); | ||||
|             } | ||||
|  | ||||
|             descriptionView.setMovementMethod(LinkMovementMethod.getInstance()); | ||||
|  | ||||
|             // parse streams | ||||
|             Vector<VideoInfo.VideoStream> streamsToUse = new Vector<>(); | ||||
|             for (VideoInfo.VideoStream i : info.video_streams) { | ||||
|             Vector<VideoStream> streamsToUse = new Vector<>(); | ||||
|             for (VideoStream i : info.video_streams) { | ||||
|                 if (useStream(i, streamsToUse)) { | ||||
|                     streamsToUse.add(i); | ||||
|                 } | ||||
| @@ -292,18 +417,27 @@ public class VideoItemDetailFragment extends Fragment { | ||||
|             }); | ||||
|             textContentLayout.setVisibility(View.VISIBLE); | ||||
|  | ||||
|             if(info.related_videos != null && !info.related_videos.isEmpty()) { | ||||
|                 initSimilarVideos(info, videoItemViewCreator); | ||||
|             } else { | ||||
|                 activity.findViewById(R.id.detailSimilarTitle).setVisibility(View.GONE); | ||||
|                 activity.findViewById(R.id.similarVideosView).setVisibility(View.GONE); | ||||
|             } | ||||
|  | ||||
|             setupActionBarHandler(info); | ||||
|  | ||||
|             if(autoPlayEnabled) { | ||||
|                 playVideo(info); | ||||
|             } | ||||
|  | ||||
|             if (android.os.Build.VERSION.SDK_INT < 18) { | ||||
|                 playVideoButton.setOnClickListener(new View.OnClickListener() { | ||||
|                     @Override | ||||
|                     public void onClick(View v) { | ||||
|                         playVideo(info); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             backgroundButton.setOnClickListener(new View.OnClickListener() { | ||||
|                 @Override | ||||
| @@ -312,20 +446,20 @@ public class VideoItemDetailFragment extends Fragment { | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             setupActionBarHandler(info); | ||||
|         } catch (java.lang.NullPointerException e) { | ||||
|             Log.w(TAG, "updateInfo(): Fragment closed before thread ended work... or else"); | ||||
|             e.printStackTrace(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void initThumbnailViews(VideoInfo info, View nextVideoFrame) { | ||||
|     private void initThumbnailViews(StreamInfo info, View nextVideoFrame) { | ||||
|         ImageView videoThumbnailView = (ImageView) activity.findViewById(R.id.detailThumbnailView); | ||||
|         ImageView uploaderThumb | ||||
|                 = (ImageView) activity.findViewById(R.id.detailUploaderThumbnailView); | ||||
|         ImageView nextVideoThumb = | ||||
|                 (ImageView) nextVideoFrame.findViewById(R.id.itemThumbnailView); | ||||
|  | ||||
|         if(info.thumbnail_url != null && !info.thumbnail_url.isEmpty()) { | ||||
|             imageLoader.displayImage(info.thumbnail_url, videoThumbnailView, | ||||
|                     displayImageOptions, new ImageLoadingListener() { | ||||
|                         @Override | ||||
| @@ -348,13 +482,20 @@ public class VideoItemDetailFragment extends Fragment { | ||||
|                         public void onLoadingCancelled(String imageUri, View view) { | ||||
|                         } | ||||
|                     }); | ||||
|         } else { | ||||
|             videoThumbnailView.setImageResource(R.drawable.dummy_thumbnail_dark); | ||||
|         } | ||||
|         if(info.uploader_thumbnail_url != null && !info.uploader_thumbnail_url.isEmpty()) { | ||||
|             imageLoader.displayImage(info.uploader_thumbnail_url, | ||||
|                     uploaderThumb, displayImageOptions, new ThumbnailLoadingListener()); | ||||
|         } | ||||
|         if(info.thumbnail_url != null && !info.thumbnail_url.isEmpty() && info.next_video != null) { | ||||
|             imageLoader.displayImage(info.next_video.thumbnail_url, | ||||
|                     nextVideoThumb, displayImageOptions, new ThumbnailLoadingListener()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void setupActionBarHandler(final VideoInfo info) { | ||||
|     private void setupActionBarHandler(final StreamInfo info) { | ||||
|         actionBarHandler.setupStreamList(info.video_streams); | ||||
|  | ||||
|         actionBarHandler.setOnShareListener(new ActionBarHandler.OnActionListener() { | ||||
| @@ -414,31 +555,50 @@ public class VideoItemDetailFragment extends Fragment { | ||||
|         actionBarHandler.setOnDownloadListener(new ActionBarHandler.OnActionListener() { | ||||
|             @Override | ||||
|             public void onActionSelected(int selectedStreamId) { | ||||
|                 //VideoInfo.VideoStream selectedStreamItem = videoStreams.get(selectedStream); | ||||
|                 VideoInfo.AudioStream audioStream = | ||||
|                         info.audio_streams.get(getPreferredAudioStreamId(info)); | ||||
|                 VideoInfo.VideoStream selectedStreamItem = info.video_streams.get(selectedStreamId); | ||||
|                 String videoSuffix = "." + MediaFormat.getSuffixById(selectedStreamItem.format); | ||||
|                 String audioSuffix = "." + MediaFormat.getSuffixById(audioStream.format); | ||||
|                 try { | ||||
|                     Bundle args = new Bundle(); | ||||
|                 args.putString(DownloadDialog.FILE_SUFFIX_VIDEO, videoSuffix); | ||||
|                 args.putString(DownloadDialog.FILE_SUFFIX_AUDIO, audioSuffix); | ||||
|                 args.putString(DownloadDialog.TITLE, info.title); | ||||
|                 args.putString(DownloadDialog.VIDEO_URL, selectedStreamItem.url); | ||||
|  | ||||
|                     // Sometimes it may be that some information is not available due to changes fo the | ||||
|                     // website which was crawled. Then the ui has to understand this and act right. | ||||
|  | ||||
|                     if (info.audio_streams != null) { | ||||
|                         AudioStream audioStream = | ||||
|                                 info.audio_streams.get(getPreferredAudioStreamId(info)); | ||||
|  | ||||
|                         String audioSuffix = "." + MediaFormat.getSuffixById(audioStream.format); | ||||
|                         args.putString(DownloadDialog.AUDIO_URL, audioStream.url); | ||||
|                         args.putString(DownloadDialog.FILE_SUFFIX_AUDIO, audioSuffix); | ||||
|                     } | ||||
|  | ||||
|                     if (info.video_streams != null) { | ||||
|                         VideoStream selectedStreamItem = info.video_streams.get(selectedStreamId); | ||||
|                         String videoSuffix = "." + MediaFormat.getSuffixById(selectedStreamItem.format); | ||||
|                         args.putString(DownloadDialog.FILE_SUFFIX_VIDEO, videoSuffix); | ||||
|                         args.putString(DownloadDialog.VIDEO_URL, selectedStreamItem.url); | ||||
|                     } | ||||
|  | ||||
|                     args.putString(DownloadDialog.TITLE, info.title); | ||||
|                     DownloadDialog downloadDialog = new DownloadDialog(); | ||||
|                     downloadDialog.setArguments(args); | ||||
|                     downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); | ||||
|                 } catch (Exception e) { | ||||
|                     Toast.makeText(VideoItemDetailFragment.this.getActivity(), | ||||
|                             R.string.could_not_setup_download_menu, Toast.LENGTH_LONG).show(); | ||||
|                     e.printStackTrace(); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         if(info.audio_streams == null) { | ||||
|             actionBarHandler.showAudioAction(false); | ||||
|         } else { | ||||
|             actionBarHandler.setOnPlayAudioListener(new ActionBarHandler.OnActionListener() { | ||||
|                 @Override | ||||
|                 public void onActionSelected(int selectedStreamId) { | ||||
|                     boolean useExternalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(activity) | ||||
|                             .getBoolean(activity.getString(R.string.use_external_audio_player_key), false); | ||||
|                     Intent intent; | ||||
|                 VideoInfo.AudioStream audioStream = | ||||
|                     AudioStream audioStream = | ||||
|                             info.audio_streams.get(getPreferredAudioStreamId(info)); | ||||
|                     if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 18) { | ||||
|                         //internal music player: explicit intent | ||||
| @@ -495,8 +655,9 @@ public class VideoItemDetailFragment extends Fragment { | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private int getPreferredAudioStreamId(final VideoInfo info) { | ||||
|     private int getPreferredAudioStreamId(final StreamInfo info) { | ||||
|         String preferredFormatString = PreferenceManager.getDefaultSharedPreferences(getActivity()) | ||||
|                 .getString(activity.getString(R.string.default_audio_format_key), "webm"); | ||||
|  | ||||
| @@ -523,12 +684,12 @@ public class VideoItemDetailFragment extends Fragment { | ||||
|         return 0; | ||||
|     } | ||||
|  | ||||
|     private void initSimilarVideos(final VideoInfo info, VideoInfoItemViewCreator videoItemViewCreator) { | ||||
|     private void initSimilarVideos(final StreamInfo info, VideoInfoItemViewCreator videoItemViewCreator) { | ||||
|         LinearLayout similarLayout = (LinearLayout) activity.findViewById(R.id.similarVideosView); | ||||
|         ArrayList<VideoPreviewInfo> similar = new ArrayList<>(info.related_videos); | ||||
|         for (final VideoPreviewInfo item : similar) { | ||||
|         ArrayList<StreamPreviewInfo> similar = new ArrayList<>(info.related_videos); | ||||
|         for (final StreamPreviewInfo item : similar) { | ||||
|             View similarView = videoItemViewCreator | ||||
|                     .getViewFromVideoInfoItem(null, similarLayout, item, getContext()); | ||||
|                     .getViewFromVideoInfoItem(null, similarLayout, item); | ||||
|  | ||||
|             similarView.setClickable(true); | ||||
|             similarView.setFocusable(true); | ||||
| @@ -591,8 +752,17 @@ public class VideoItemDetailFragment extends Fragment { | ||||
|                 .show(); | ||||
|     } | ||||
|  | ||||
|     private boolean useStream(VideoInfo.VideoStream stream, Vector<VideoInfo.VideoStream> streams) { | ||||
|         for(VideoInfo.VideoStream i : streams) { | ||||
|     private void onNotSpecifiedContentErrorWithMessage(int resourceId) { | ||||
|         ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detailThumbnailView); | ||||
|         progressBar.setVisibility(View.GONE); | ||||
|         thumbnailView.setImageBitmap(BitmapFactory.decodeResource( | ||||
|                 getResources(), R.drawable.not_available_monkey)); | ||||
|         Toast.makeText(activity, resourceId, Toast.LENGTH_LONG) | ||||
|                 .show(); | ||||
|     } | ||||
|  | ||||
|     private boolean useStream(VideoStream stream, Vector<VideoStream> streams) { | ||||
|         for(VideoStream i : streams) { | ||||
|             if(i.resolution.equals(stream.resolution)) { | ||||
|                 return false; | ||||
|             } | ||||
| @@ -633,7 +803,9 @@ public class VideoItemDetailFragment extends Fragment { | ||||
|     public void onActivityCreated(Bundle savedInstanceBundle) { | ||||
|         super.onActivityCreated(savedInstanceBundle); | ||||
|         Activity a = getActivity(); | ||||
|         if (android.os.Build.VERSION.SDK_INT < 18) { | ||||
|             playVideoButton = (FloatingActionButton) a.findViewById(R.id.playVideoButton); | ||||
|         } | ||||
|         thumbnailWindowLayout = a.findViewById(R.id.detailVideoThumbnailWindowLayout); | ||||
|         Button backgroundButton = (Button) | ||||
|                 a.findViewById(R.id.detailVideoThumbnailWindowBackgroundButton); | ||||
| @@ -641,7 +813,7 @@ public class VideoItemDetailFragment extends Fragment { | ||||
|         // Sometimes when this fragment is not visible it still gets initiated | ||||
|         // then we must not try to access objects of this fragment. | ||||
|         // Otherwise the applications would crash. | ||||
|         if(playVideoButton != null) { | ||||
|         if(backgroundButton != null) { | ||||
|             try { | ||||
|                 streamingServiceId = getArguments().getInt(STREAMING_SERVICE); | ||||
|                 StreamingService streamingService = ServiceList.getService(streamingServiceId); | ||||
| @@ -654,13 +826,15 @@ public class VideoItemDetailFragment extends Fragment { | ||||
|                 e.printStackTrace(); | ||||
|             } | ||||
|  | ||||
|             // todo: Fix this workaround (probably with a better design), so that older android | ||||
|             // versions don't have problems rendering the thumbnail right. | ||||
|             if(Build.VERSION.SDK_INT >= 18) { | ||||
|                 ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detailThumbnailView); | ||||
|                 thumbnailView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { | ||||
|                     // This is used to synchronize the thumbnailWindowButton and the playVideoButton | ||||
|                     // inside the ScrollView with the actual size of the thumbnail. | ||||
|                     //todo: onLayoutChage sometimes not triggered | ||||
|                     // background buttons area seem to overlap the thumbnail view | ||||
|                     // So although you just clicked slightly beneath the thumbnail the action still | ||||
|                     // gets triggered. | ||||
|                     @Override | ||||
|                     public void onLayoutChange(View v, int left, int top, int right, int bottom, | ||||
|                                                int oldLeft, int oldTop, int oldRight, int oldBottom) { | ||||
| @@ -678,9 +852,9 @@ public class VideoItemDetailFragment extends Fragment { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void playVideo(final VideoInfo info) { | ||||
|     public void playVideo(final StreamInfo info) { | ||||
|         // ----------- THE MAGIC MOMENT --------------- | ||||
|         VideoInfo.VideoStream selectedVideoStream = | ||||
|         VideoStream selectedVideoStream = | ||||
|                 info.video_streams.get(actionBarHandler.getSelectedVideoStream()); | ||||
|  | ||||
|         if (PreferenceManager.getDefaultSharedPreferences(activity) | ||||
| @@ -689,12 +863,11 @@ public class VideoItemDetailFragment extends Fragment { | ||||
|             // External Player | ||||
|             Intent intent = new Intent(); | ||||
|             try { | ||||
|                 intent.setAction(Intent.ACTION_VIEW); | ||||
|  | ||||
|                 intent.setDataAndType(Uri.parse(selectedVideoStream.url), | ||||
|                         MediaFormat.getMimeById(selectedVideoStream.format)); | ||||
|                 intent.putExtra(Intent.EXTRA_TITLE, info.title); | ||||
|                 intent.putExtra("title", info.title); | ||||
|                 intent.setAction(Intent.ACTION_VIEW) | ||||
|                         .setDataAndType(Uri.parse(selectedVideoStream.url), | ||||
|                             MediaFormat.getMimeById(selectedVideoStream.format)) | ||||
|                         .putExtra(Intent.EXTRA_TITLE, info.title) | ||||
|                         .putExtra("title", info.title); | ||||
|  | ||||
|                 activity.startActivity(intent);      // HERE !!! | ||||
|             } catch (Exception e) { | ||||
| @@ -704,9 +877,9 @@ public class VideoItemDetailFragment extends Fragment { | ||||
|                         .setPositiveButton(R.string.install, new DialogInterface.OnClickListener() { | ||||
|                             @Override | ||||
|                             public void onClick(DialogInterface dialog, int which) { | ||||
|                                 Intent intent = new Intent(); | ||||
|                                 intent.setAction(Intent.ACTION_VIEW); | ||||
|                                 intent.setData(Uri.parse(activity.getString(R.string.fdroid_vlc_url))); | ||||
|                                 Intent intent = new Intent() | ||||
|                                         .setAction(Intent.ACTION_VIEW) | ||||
|                                         .setData(Uri.parse(activity.getString(R.string.fdroid_vlc_url))); | ||||
|                                 activity.startActivity(intent); | ||||
|                             } | ||||
|                         }) | ||||
| @@ -718,15 +891,43 @@ public class VideoItemDetailFragment extends Fragment { | ||||
|                         }); | ||||
|                 builder.create().show(); | ||||
|             } | ||||
|         } else { | ||||
|             if (PreferenceManager.getDefaultSharedPreferences(activity) | ||||
|                     .getBoolean(activity.getString(R.string.use_exoplayer_key), false)) { | ||||
|  | ||||
|                 // exo player | ||||
|  | ||||
|                 if(info.dashMpdUrl != null && !info.dashMpdUrl.isEmpty()) { | ||||
|                     // try dash | ||||
|                     Intent intent = new Intent(activity, ExoPlayerActivity.class) | ||||
|                             .setData(Uri.parse(info.dashMpdUrl)) | ||||
|                             .putExtra(ExoPlayerActivity.CONTENT_TYPE_EXTRA, Util.TYPE_DASH); | ||||
|                     startActivity(intent); | ||||
|                 } else if((info.audio_streams != null  && !info.audio_streams.isEmpty()) && | ||||
|                         (info.video_only_streams != null && !info.video_only_streams.isEmpty())) { | ||||
|                     // try smooth streaming | ||||
|  | ||||
|                 } else { | ||||
|                     //default streaming | ||||
|                     Intent intent = new Intent(activity, ExoPlayerActivity.class) | ||||
|                             .setDataAndType(Uri.parse(selectedVideoStream.url), | ||||
|                                 MediaFormat.getMimeById(selectedVideoStream.format)) | ||||
|                             .putExtra(ExoPlayerActivity.CONTENT_TYPE_EXTRA, Util.TYPE_OTHER); | ||||
|  | ||||
|                     activity.startActivity(intent);      // HERE !!! | ||||
|                 } | ||||
|                 //------------- | ||||
|  | ||||
|             } else { | ||||
|                 // Internal Player | ||||
|             Intent intent = new Intent(activity, PlayVideoActivity.class); | ||||
|             intent.putExtra(PlayVideoActivity.VIDEO_TITLE, info.title); | ||||
|             intent.putExtra(PlayVideoActivity.STREAM_URL, selectedVideoStream.url); | ||||
|             intent.putExtra(PlayVideoActivity.VIDEO_URL, info.webpage_url); | ||||
|             intent.putExtra(PlayVideoActivity.START_POSITION, info.start_position); | ||||
|                 Intent intent = new Intent(activity, PlayVideoActivity.class) | ||||
|                         .putExtra(PlayVideoActivity.VIDEO_TITLE, info.title) | ||||
|                         .putExtra(PlayVideoActivity.STREAM_URL, selectedVideoStream.url) | ||||
|                         .putExtra(PlayVideoActivity.VIDEO_URL, info.webpage_url) | ||||
|                         .putExtra(PlayVideoActivity.START_POSITION, info.start_position); | ||||
|                 activity.startActivity(intent);     //also HERE !!! | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // -------------------------------------------- | ||||
|     } | ||||
|   | ||||
| @@ -2,8 +2,9 @@ package org.schabi.newpipe; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.media.AudioManager; | ||||
| import android.content.SharedPreferences; | ||||
| import android.os.Bundle; | ||||
| import android.os.Handler; | ||||
| import android.preference.PreferenceManager; | ||||
| import android.support.v4.app.NavUtils; | ||||
| import android.support.v7.app.AppCompatActivity; | ||||
| @@ -14,11 +15,16 @@ import android.view.MenuInflater; | ||||
| import android.view.MenuItem; | ||||
| import android.view.View; | ||||
| import android.view.inputmethod.InputMethodManager; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.ExtractionException; | ||||
| import org.schabi.newpipe.extractor.SearchEngine; | ||||
| import org.schabi.newpipe.extractor.ServiceList; | ||||
| import org.schabi.newpipe.extractor.StreamingService; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.util.ArrayList; | ||||
|  | ||||
| import org.schabi.newpipe.crawler.VideoPreviewInfo; | ||||
| import org.schabi.newpipe.crawler.ServiceList; | ||||
| import java.util.Vector; | ||||
|  | ||||
| /** | ||||
|  * Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org> | ||||
| @@ -62,6 +68,10 @@ public class VideoItemListActivity extends AppCompatActivity | ||||
|     private VideoItemDetailFragment videoFragment = null; | ||||
|     private Menu menu = null; | ||||
|  | ||||
|     private SuggestionListAdapter suggestionListAdapter; | ||||
|     private SuggestionSearchRunnable suggestionSearchRunnable; | ||||
|     private Thread searchThread; | ||||
|  | ||||
|     private class SearchVideoQueryListener implements SearchView.OnQueryTextListener { | ||||
|  | ||||
|         @Override | ||||
| @@ -79,6 +89,8 @@ public class VideoItemListActivity extends AppCompatActivity | ||||
|                             getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); | ||||
|                 } catch(NullPointerException e) { | ||||
|                     Log.e(TAG, "Could not get widget with focus"); | ||||
|                     Toast.makeText(VideoItemListActivity.this, "Could not get widget with focus", | ||||
|                             Toast.LENGTH_SHORT).show(); | ||||
|                     e.printStackTrace(); | ||||
|                 } | ||||
|                 // clear focus | ||||
| @@ -90,18 +102,92 @@ public class VideoItemListActivity extends AppCompatActivity | ||||
|             } catch(Exception e) { | ||||
|                 e.printStackTrace(); | ||||
|             } | ||||
|             View bg = findViewById(R.id.mainBG); | ||||
|             bg.setVisibility(View.GONE); | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public boolean onQueryTextChange(String newText) { | ||||
|             if(!newText.isEmpty()) { | ||||
|                 searchSuggestions(newText); | ||||
|             } | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|     } | ||||
|     private class SearchSuggestionListener implements SearchView.OnSuggestionListener{ | ||||
|  | ||||
|         private SearchView searchView; | ||||
|  | ||||
|         private SearchSuggestionListener(SearchView searchView) { | ||||
|             this.searchView = searchView; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public boolean onSuggestionSelect(int position) { | ||||
|             String suggestion = suggestionListAdapter.getSuggestion(position); | ||||
|             searchView.setQuery(suggestion,true); | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public boolean onSuggestionClick(int position) { | ||||
|             String suggestion = suggestionListAdapter.getSuggestion(position); | ||||
|             searchView.setQuery(suggestion,true); | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private class SuggestionResultRunnable implements Runnable{ | ||||
|  | ||||
|         private ArrayList<String>suggestions; | ||||
|  | ||||
|         private SuggestionResultRunnable(ArrayList<String> suggestions) { | ||||
|             this.suggestions = suggestions; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void run() { | ||||
|             suggestionListAdapter.updateAdapter(suggestions); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private class SuggestionSearchRunnable implements Runnable{ | ||||
|         private final int serviceId; | ||||
|         private final String query; | ||||
|         final Handler h = new Handler(); | ||||
|         private Context context; | ||||
|         private SuggestionSearchRunnable(int serviceId, String query) { | ||||
|             this.serviceId = serviceId; | ||||
|             this.query = query; | ||||
|             context = VideoItemListActivity.this; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void run() { | ||||
|             try { | ||||
|                 SearchEngine engine = | ||||
|                         ServiceList.getService(serviceId).getSearchEngineInstance(new Downloader()); | ||||
|                 SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); | ||||
|                 String searchLanguageKey = context.getString(R.string.search_language_key); | ||||
|                 String searchLanguage = sp.getString(searchLanguageKey, | ||||
|                         getString(R.string.default_language_value)); | ||||
|                 ArrayList<String>suggestions = engine.suggestionList(query,searchLanguage,new Downloader()); | ||||
|                 h.post(new SuggestionResultRunnable(suggestions)); | ||||
|             } catch (ExtractionException e) { | ||||
|                 ErrorActivity.reportError(h, VideoItemListActivity.this, e, null, findViewById(R.id.videoitem_list), | ||||
|                         ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, | ||||
|                                 ServiceList.getNameOfService(serviceId), query, R.string.parsing_error)); | ||||
|                 e.printStackTrace(); | ||||
|             } catch (IOException e) { | ||||
|                 postNewErrorToast(h, R.string.network_error); | ||||
|                 e.printStackTrace(); | ||||
|             } catch (Exception e) { | ||||
|                 ErrorActivity.reportError(h, VideoItemListActivity.this, e, null, findViewById(R.id.videoitem_list), | ||||
|                         ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, | ||||
|                                 ServiceList.getNameOfService(serviceId), query, R.string.general_error)); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     /** | ||||
|      * Whether or not the activity is in two-pane mode, i.e. running on a tablet | ||||
|      * device. | ||||
| @@ -112,37 +198,23 @@ public class VideoItemListActivity extends AppCompatActivity | ||||
|     protected void onCreate(Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         setContentView(R.layout.activity_videoitem_list); | ||||
|         StreamingService streamingService = null; | ||||
|  | ||||
|         View bg = findViewById(R.id.mainBG); | ||||
|         bg.setVisibility(View.VISIBLE); | ||||
|  | ||||
|         try { | ||||
|             //------ todo: remove this line when multiservice support is implemented ------ | ||||
|             currentStreamingServiceId = ServiceList.getIdOfService("Youtube"); | ||||
|             streamingService = ServiceList.getService(currentStreamingServiceId); | ||||
|         } catch (Exception e) { | ||||
|             e.printStackTrace(); | ||||
|             ErrorActivity.reportError(VideoItemListActivity.this, e, null, findViewById(R.id.videoitem_list), | ||||
|                     ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, | ||||
|                             ServiceList.getNameOfService(currentStreamingServiceId), "", R.string.general_error)); | ||||
|         } | ||||
|         //----------------------------------------------------------------------------- | ||||
|         //to solve issue 38 | ||||
|         listFragment = (VideoItemListFragment) getSupportFragmentManager() | ||||
|                 .findFragmentById(R.id.videoitem_list); | ||||
|         listFragment.setStreamingService(ServiceList.getService(currentStreamingServiceId)); | ||||
|  | ||||
|         Bundle arguments = getIntent().getExtras(); | ||||
|  | ||||
|         if(arguments != null) { | ||||
|             //Parcelable[] p = arguments.getParcelableArray(VIDEO_INFO_ITEMS); | ||||
|             ArrayList<VideoPreviewInfo> p = arguments.getParcelableArrayList(VIDEO_INFO_ITEMS); | ||||
|             if(p != null) { | ||||
|                 mode = PRESENT_VIDEOS_MODE; | ||||
|                 try { | ||||
|                     //noinspection ConstantConditions | ||||
|                     getSupportActionBar().setDisplayHomeAsUpEnabled(true); | ||||
|                 } catch (NullPointerException e) { | ||||
|                     Log.e(TAG, "Could not get SupportActionBar"); | ||||
|                     e.printStackTrace(); | ||||
|                 } | ||||
|  | ||||
|                 listFragment.present(p); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         listFragment.setStreamingService(streamingService); | ||||
|  | ||||
|         if(savedInstanceState != null | ||||
|                 && mode != PRESENT_VIDEOS_MODE) { | ||||
| @@ -173,7 +245,13 @@ public class VideoItemListActivity extends AppCompatActivity | ||||
|                 // the support version on SearchView, so it needs to be set programmatically. | ||||
|                 searchView.setIconifiedByDefault(false); | ||||
|                 searchView.setIconified(false); | ||||
|                 if(!searchQuery.isEmpty()) { | ||||
|                     searchView.setQuery(searchQuery,false); | ||||
|                 } | ||||
|                 searchView.setOnQueryTextListener(new SearchVideoQueryListener()); | ||||
|                 suggestionListAdapter = new SuggestionListAdapter(this); | ||||
|                 searchView.setSuggestionsAdapter(suggestionListAdapter); | ||||
|                 searchView.setOnSuggestionListener(new SearchSuggestionListener(searchView)); | ||||
|             } else { | ||||
|                 searchView.setVisibility(View.GONE); | ||||
|             } | ||||
| @@ -198,14 +276,14 @@ public class VideoItemListActivity extends AppCompatActivity | ||||
|                 getSupportFragmentManager() | ||||
|                         .findFragmentById(R.id.videoitem_list)) | ||||
|                 .getListAdapter(); | ||||
|         String webpage_url = listAdapter.getVideoList().get((int) Long.parseLong(id)).webpage_url; | ||||
|         String webpageUrl = listAdapter.getVideoList().get((int) Long.parseLong(id)).webpage_url; | ||||
|         if (mTwoPane) { | ||||
|             // In two-pane mode, show the detail view in this activity by | ||||
|             // adding or replacing the detail fragment using a | ||||
|             // fragment transaction. | ||||
|             Bundle arguments = new Bundle(); | ||||
|             //arguments.putString(VideoItemDetailFragment.ARG_ITEM_ID, id); | ||||
|             arguments.putString(VideoItemDetailFragment.VIDEO_URL, webpage_url); | ||||
|             arguments.putString(VideoItemDetailFragment.VIDEO_URL, webpageUrl); | ||||
|             arguments.putInt(VideoItemDetailFragment.STREAMING_SERVICE, currentStreamingServiceId); | ||||
|             videoFragment = new VideoItemDetailFragment(); | ||||
|             videoFragment.setArguments(arguments); | ||||
| @@ -224,7 +302,7 @@ public class VideoItemListActivity extends AppCompatActivity | ||||
|             // for the selected item ID. | ||||
|             Intent detailIntent = new Intent(this, VideoItemDetailActivity.class); | ||||
|             //detailIntent.putExtra(VideoItemDetailFragment.ARG_ITEM_ID, id); | ||||
|             detailIntent.putExtra(VideoItemDetailFragment.VIDEO_URL, webpage_url); | ||||
|             detailIntent.putExtra(VideoItemDetailFragment.VIDEO_URL, webpageUrl); | ||||
|             detailIntent.putExtra(VideoItemDetailFragment.STREAMING_SERVICE, currentStreamingServiceId); | ||||
|             startActivity(detailIntent); | ||||
|         } | ||||
| @@ -243,7 +321,13 @@ public class VideoItemListActivity extends AppCompatActivity | ||||
|             searchView.setFocusable(false); | ||||
|             searchView.setOnQueryTextListener( | ||||
|                     new SearchVideoQueryListener()); | ||||
|  | ||||
|             suggestionListAdapter = new SuggestionListAdapter(this); | ||||
|             searchView.setSuggestionsAdapter(suggestionListAdapter); | ||||
|             searchView.setOnSuggestionListener(new SearchSuggestionListener(searchView)); | ||||
|             if(!searchQuery.isEmpty()) { | ||||
|                 searchView.setQuery(searchQuery,false); | ||||
|                 searchView.setIconifiedByDefault(false); | ||||
|             } | ||||
|         } else if (videoFragment != null){ | ||||
|             videoFragment.onCreateOptionsMenu(menu, inflater); | ||||
|         } else { | ||||
| @@ -269,6 +353,14 @@ public class VideoItemListActivity extends AppCompatActivity | ||||
|                 startActivity(intent); | ||||
|                 return true; | ||||
|             } | ||||
|             case R.id.action_report_error: { | ||||
|                 ErrorActivity.reportError(VideoItemListActivity.this, new Vector<Exception>(), | ||||
|                         null, null, | ||||
|                         ErrorActivity.ErrorInfo.make(ErrorActivity.USER_REPORT, | ||||
|                                 ServiceList.getNameOfService(currentStreamingServiceId), | ||||
|                                 "user_report", R.string.user_report)); | ||||
|                 return true; | ||||
|             } | ||||
|             default: | ||||
|                 return videoFragment.onOptionsItemSelected(item) || | ||||
|                     super.onOptionsItemSelected(item); | ||||
| @@ -287,4 +379,22 @@ public class VideoItemListActivity extends AppCompatActivity | ||||
|         outState.putString(QUERY, searchQuery); | ||||
|         outState.putInt(STREAMING_SERVICE, currentStreamingServiceId); | ||||
|     } | ||||
|  | ||||
|     private void searchSuggestions(String query) { | ||||
|         suggestionSearchRunnable = | ||||
|                 new SuggestionSearchRunnable(currentStreamingServiceId, query); | ||||
|         searchThread = new Thread(suggestionSearchRunnable); | ||||
|         searchThread.start(); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     private void postNewErrorToast(Handler h, final int stringResource) { | ||||
|         h.post(new Runnable() { | ||||
|             @Override | ||||
|             public void run() { | ||||
|                 Toast.makeText(VideoItemListActivity.this, getString(stringResource), | ||||
|                         Toast.LENGTH_SHORT).show(); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,9 +1,8 @@ | ||||
| package org.schabi.newpipe; | ||||
|  | ||||
| import android.app.Activity; | ||||
| import android.content.Context; | ||||
| import android.content.SharedPreferences; | ||||
| import android.graphics.Bitmap; | ||||
| import android.graphics.BitmapFactory; | ||||
| import android.os.Bundle; | ||||
| import android.os.Handler; | ||||
| import android.preference.PreferenceManager; | ||||
| @@ -16,14 +15,13 @@ import android.widget.ListView; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.net.URL; | ||||
| import java.util.List; | ||||
| import java.util.Vector; | ||||
|  | ||||
| import org.schabi.newpipe.crawler.CrawlingException; | ||||
| import org.schabi.newpipe.crawler.VideoPreviewInfo; | ||||
| import org.schabi.newpipe.crawler.SearchEngine; | ||||
| import org.schabi.newpipe.crawler.StreamingService; | ||||
| import org.schabi.newpipe.extractor.ExtractionException; | ||||
| import org.schabi.newpipe.extractor.SearchResult; | ||||
| import org.schabi.newpipe.extractor.StreamPreviewInfo; | ||||
| import org.schabi.newpipe.extractor.SearchEngine; | ||||
| import org.schabi.newpipe.extractor.StreamingService; | ||||
|  | ||||
|  | ||||
| /** | ||||
| @@ -71,9 +69,9 @@ public class VideoItemListFragment extends ListFragment { | ||||
|     private boolean loadingNextPage = true; | ||||
|  | ||||
|     private class ResultRunnable implements Runnable { | ||||
|         private final SearchEngine.Result result; | ||||
|         private final SearchResult result; | ||||
|         private final int requestId; | ||||
|         public ResultRunnable(SearchEngine.Result result, int requestId) { | ||||
|         public ResultRunnable(SearchResult result, int requestId) { | ||||
|             this.result = result; | ||||
|             this.requestId = requestId; | ||||
|         } | ||||
| @@ -104,92 +102,60 @@ public class VideoItemListFragment extends ListFragment { | ||||
|         } | ||||
|         @Override | ||||
|         public void run() { | ||||
|             SearchResult result = null; | ||||
|             try { | ||||
|                 SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext()); | ||||
|                 String searchLanguageKey = getContext().getString(R.string.search_language_key); | ||||
|                 String searchLanguage = sp.getString(searchLanguageKey, | ||||
|                         getString(R.string.default_language_value)); | ||||
|                 SearchEngine.Result result = engine.search(query, page, searchLanguage, | ||||
|                         new Downloader()); | ||||
|                 result = SearchResult | ||||
|                         .getSearchResult(engine, query, page, searchLanguage, new Downloader()); | ||||
|  | ||||
|                 Log.i(TAG, "language code passed:\""+searchLanguage+"\""); | ||||
|                 if(runs) { | ||||
|                     h.post(new ResultRunnable(result, requestId)); | ||||
|                 } | ||||
|  | ||||
|                 // look for errors during extraction | ||||
|                 // soft errors: | ||||
|                 if(result != null && | ||||
|                         !result.errors.isEmpty()) { | ||||
|                     Log.e(TAG, "OCCURRED ERRORS DURING SEARCH EXTRACTION:"); | ||||
|                     for(Exception e : result.errors) { | ||||
|                         e.printStackTrace(); | ||||
|                         Log.e(TAG, "------"); | ||||
|                     } | ||||
|  | ||||
|                     Activity a = getActivity(); | ||||
|                     View rootView = a.findViewById(R.id.videoitem_list); | ||||
|                     ErrorActivity.reportError(h, getActivity(), result.errors, null, rootView, | ||||
|                             ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, | ||||
|                         /* todo: this shoudl not be assigned static */ "Youtube", query, R.string.light_parsing_error)); | ||||
|  | ||||
|                 } | ||||
|                 // hard errors: | ||||
|             } catch(IOException e) { | ||||
|                 postNewErrorToast(h, R.string.network_error); | ||||
|                 postNewNothingFoundToast(h, R.string.network_error); | ||||
|                 e.printStackTrace(); | ||||
|             } catch(CrawlingException ce) { | ||||
|                 postNewErrorToast(h, R.string.parsing_error); | ||||
|                 ce.printStackTrace(); | ||||
|             } catch(SearchEngine.NothingFoundException e) { | ||||
|                 postNewErrorToast(h, e.getMessage()); | ||||
|             } catch(ExtractionException e) { | ||||
|                 ErrorActivity.reportError(h, getActivity(), e, null, null, | ||||
|                         ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, | ||||
|                         /* todo: this shoudl not be assigned static */ "Youtube", query, R.string.parsing_error)); | ||||
|                 //postNewErrorToast(h, R.string.parsing_error); | ||||
|                 e.printStackTrace(); | ||||
|  | ||||
|             } catch(Exception e) { | ||||
|                 postNewErrorToast(h, R.string.general_error); | ||||
|                 ErrorActivity.reportError(h, getActivity(), e, null, null, | ||||
|                         ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, | ||||
|                         /* todo: this shoudl not be assigned static */ "Youtube", query, R.string.general_error)); | ||||
|  | ||||
|                 e.printStackTrace(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| /* | ||||
| <<< | ||||
|     private class LoadThumbsRunnable implements Runnable { | ||||
|         private final Vector<String> thumbnailUrlList = new Vector<>(); | ||||
|         private final Vector<Boolean> downloadedList; | ||||
|         final Handler h = new Handler(); | ||||
|         private volatile boolean run = true; | ||||
|         private final int requestId; | ||||
|         public LoadThumbsRunnable(Vector<VideoPreviewInfo> videoList, | ||||
|                                   Vector<Boolean> downloadedList, int requestId) { | ||||
|             for(VideoPreviewInfo item : videoList) { | ||||
|                 thumbnailUrlList.add(item.thumbnail_url); | ||||
|             } | ||||
|             this.downloadedList = downloadedList; | ||||
|             this.requestId = requestId; | ||||
|         } | ||||
|         public void terminate() { | ||||
|             run = false; | ||||
|         } | ||||
|         public boolean isRunning() { | ||||
|             return run; | ||||
|         } | ||||
|         @Override | ||||
|         public void run() { | ||||
|             for(int i = 0; i < thumbnailUrlList.size() && run; i++) { | ||||
|                 if(!downloadedList.get(i)) { | ||||
|                     Bitmap thumbnail; | ||||
|                     try { | ||||
|                         //todo: make bitmaps not bypass tor | ||||
|                         thumbnail = BitmapFactory.decodeStream( | ||||
|                                 new URL(thumbnailUrlList.get(i)).openConnection().getInputStream()); | ||||
|                         h.post(new SetThumbnailRunnable(i, thumbnail, requestId)); | ||||
|                     } catch (Exception e) { | ||||
|                         e.printStackTrace(); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private class SetThumbnailRunnable implements Runnable { | ||||
|         private final int index; | ||||
|         private final Bitmap thumbnail; | ||||
|         private final int requestId; | ||||
|         public SetThumbnailRunnable(int index, Bitmap thumbnail, int requestId) { | ||||
|             this.index = index; | ||||
|             this.thumbnail = thumbnail; | ||||
|             this.requestId = requestId; | ||||
|         } | ||||
|         @Override | ||||
|         public void run() { | ||||
|             if(requestId == currentRequestId) { | ||||
|                 videoListAdapter.updateDownloadedThumbnailList(index); | ||||
|                 videoListAdapter.setThumbnail(index, thumbnail); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| ======= | ||||
| >>>>>>> 6d1b4652fc98e5c2d5e19b0f98ba38a731137a70 | ||||
| */ | ||||
|     public void present(List<VideoPreviewInfo> videoList) { | ||||
|     public void present(List<StreamPreviewInfo> videoList) { | ||||
|         mode = PRESENT_VIDEOS_MODE; | ||||
|         setListShown(true); | ||||
|         getListView().smoothScrollToPosition(0); | ||||
| @@ -219,7 +185,7 @@ public class VideoItemListFragment extends ListFragment { | ||||
|     private void startSearch(String query, int page) { | ||||
|         currentRequestId++; | ||||
|         terminateThreads(); | ||||
|         searchRunnable = new SearchRunnable(streamingService.getSearchEngineInstance(), | ||||
|         searchRunnable = new SearchRunnable(streamingService.getSearchEngineInstance(new Downloader()), | ||||
|                                             query, page, currentRequestId); | ||||
|         searchThread = new Thread(searchRunnable); | ||||
|         searchThread.start(); | ||||
| @@ -229,28 +195,29 @@ public class VideoItemListFragment extends ListFragment { | ||||
|         this.streamingService = streamingService; | ||||
|     } | ||||
|  | ||||
|     private void updateListOnResult(SearchEngine.Result result, int requestId) { | ||||
|     private void updateListOnResult(SearchResult result, int requestId) { | ||||
|         if(requestId == currentRequestId) { | ||||
|             setListShown(true); | ||||
|             if (result.resultList.isEmpty()) { | ||||
|                 Toast.makeText(getActivity(), result.errorMessage, Toast.LENGTH_LONG).show(); | ||||
|             } else { | ||||
|                 if (!result.suggestion.isEmpty()) { | ||||
|                     Toast.makeText(getActivity(), getString(R.string.did_you_mean) + result.suggestion + " ?", | ||||
|                             Toast.LENGTH_LONG).show(); | ||||
|                 } | ||||
|             updateList(result.resultList); | ||||
|             if(!result.suggestion.isEmpty()) { | ||||
|                 Toast.makeText(getActivity(), | ||||
|                         String.format(getString(R.string.did_you_mean), result.suggestion), | ||||
|                         Toast.LENGTH_LONG).show(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void updateList(List<VideoPreviewInfo> list) { | ||||
|     private void updateList(List<StreamPreviewInfo> list) { | ||||
|         try { | ||||
|             videoListAdapter.addVideoList(list); | ||||
|             terminateThreads(); | ||||
|         } catch(java.lang.IllegalStateException e) { | ||||
|             Toast.makeText(getActivity(), "Trying to set value while activity doesn't exist anymore.", | ||||
|                     Toast.LENGTH_SHORT).show(); | ||||
|             Log.w(TAG, "Trying to set value while activity doesn't exist anymore."); | ||||
|         } catch(Exception e) { | ||||
|             Toast.makeText(getActivity(), getString(R.string.general_error), | ||||
|                     Toast.LENGTH_SHORT).show(); | ||||
|             e.printStackTrace(); | ||||
|         } finally { | ||||
|             loadingNextPage = false; | ||||
| @@ -377,14 +344,25 @@ public class VideoItemListFragment extends ListFragment { | ||||
|         mActivatedPosition = position; | ||||
|     } | ||||
|  | ||||
|     private void postNewErrorToast(Handler h, final int stringResource) { | ||||
|     private void postNewErrorToast(Handler h, final String message) { | ||||
|         h.post(new Runnable() { | ||||
|             @Override | ||||
|             public void run() { | ||||
|                 setListShown(true); | ||||
|                 Toast.makeText(getActivity(), getString(R.string.network_error), | ||||
|                 Toast.makeText(getActivity(), message, | ||||
|                         Toast.LENGTH_SHORT).show(); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     private void postNewNothingFoundToast(Handler h, final int stringResource) { | ||||
|         h.post(new Runnable() { | ||||
|             @Override | ||||
|             public void run() { | ||||
|                 setListShown(true); | ||||
|                 Toast.makeText(getActivity(), getString(stringResource), | ||||
|                         Toast.LENGTH_LONG).show(); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -8,7 +8,7 @@ import android.view.ViewGroup; | ||||
| import android.widget.BaseAdapter; | ||||
| import android.widget.ListView; | ||||
|  | ||||
| import org.schabi.newpipe.crawler.VideoPreviewInfo; | ||||
| import org.schabi.newpipe.extractor.StreamPreviewInfo; | ||||
|  | ||||
| import java.util.List; | ||||
| import java.util.Vector; | ||||
| @@ -36,7 +36,7 @@ import java.util.Vector; | ||||
| class VideoListAdapter extends BaseAdapter { | ||||
|     private final Context context; | ||||
|     private final VideoInfoItemViewCreator viewCreator; | ||||
|     private Vector<VideoPreviewInfo> videoList = new Vector<>(); | ||||
|     private Vector<StreamPreviewInfo> videoList = new Vector<>(); | ||||
|     private final ListView listView; | ||||
|  | ||||
|     public VideoListAdapter(Context context, VideoItemListFragment videoListFragment) { | ||||
| @@ -47,7 +47,7 @@ class VideoListAdapter extends BaseAdapter { | ||||
|         this.context = context; | ||||
|     } | ||||
|  | ||||
|     public void addVideoList(List<VideoPreviewInfo> videos) { | ||||
|     public void addVideoList(List<StreamPreviewInfo> videos) { | ||||
|         videoList.addAll(videos); | ||||
|         notifyDataSetChanged(); | ||||
|     } | ||||
| @@ -57,7 +57,7 @@ class VideoListAdapter extends BaseAdapter { | ||||
|         notifyDataSetChanged(); | ||||
|     } | ||||
|  | ||||
|     public Vector<VideoPreviewInfo> getVideoList() { | ||||
|     public Vector<StreamPreviewInfo> getVideoList() { | ||||
|         return videoList; | ||||
|     } | ||||
|  | ||||
| @@ -78,7 +78,7 @@ class VideoListAdapter extends BaseAdapter { | ||||
|  | ||||
|     @Override | ||||
|     public View getView(int position, View convertView, ViewGroup parent) { | ||||
|         convertView = viewCreator.getViewFromVideoInfoItem(convertView, parent, videoList.get(position), context); | ||||
|         convertView = viewCreator.getViewFromVideoInfoItem(convertView, parent, videoList.get(position)); | ||||
|  | ||||
|         if(listView.isItemChecked(position)) { | ||||
|             convertView.setBackgroundColor(ContextCompat.getColor(context,R.color.light_youtube_primary_color)); | ||||
|   | ||||
| @@ -1,191 +0,0 @@ | ||||
| package org.schabi.newpipe.crawler; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.util.List; | ||||
| import java.util.Vector; | ||||
|  | ||||
| /** | ||||
|  * Created by Christian Schabesberger on 26.08.15. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org> | ||||
|  * VideoInfo.java is part of NewPipe. | ||||
|  * | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * NewPipe is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| /**Info object for opened videos, ie the video ready to play.*/ | ||||
| @SuppressWarnings("ALL") | ||||
| public class VideoInfo extends AbstractVideoInfo { | ||||
|  | ||||
|     /**Fills out the video info fields which are common to all services. | ||||
|      * Probably needs to be overridden by subclasses*/ | ||||
|     public static VideoInfo getVideoInfo(StreamExtractor extractor, Downloader downloader) | ||||
|             throws CrawlingException, IOException { | ||||
|         VideoInfo videoInfo = new VideoInfo(); | ||||
|  | ||||
|         VideoUrlIdHandler uiconv = extractor.getUrlIdConverter(); | ||||
|  | ||||
|  | ||||
|         videoInfo.webpage_url = extractor.getPageUrl(); | ||||
|         videoInfo.title = extractor.getTitle(); | ||||
|         videoInfo.duration = extractor.getLength(); | ||||
|         videoInfo.uploader = extractor.getUploader(); | ||||
|         videoInfo.description = extractor.getDescription(); | ||||
|         videoInfo.view_count = extractor.getViews(); | ||||
|         videoInfo.upload_date = extractor.getUploadDate(); | ||||
|         videoInfo.thumbnail_url = extractor.getThumbnailUrl(); | ||||
|         videoInfo.id = uiconv.getVideoId(extractor.getPageUrl()); | ||||
|         //todo: make this quick and dirty solution a real fallback | ||||
|         // The front end should be notified that the dash mpd could not be downloaded | ||||
|         // although not getting the dash mpd is not the end of the world, therfore | ||||
|         // we continue. | ||||
|         try { | ||||
|             videoInfo.dashMpdUrl = extractor.getDashMpdUrl(); | ||||
|         } catch(Exception e) { | ||||
|             e.printStackTrace(); | ||||
|         } | ||||
|         /** Load and extract audio*/ | ||||
|         videoInfo.audio_streams = extractor.getAudioStreams(); | ||||
|         if(videoInfo.dashMpdUrl != null && !videoInfo.dashMpdUrl.isEmpty()) { | ||||
|             if(videoInfo.audio_streams == null) { | ||||
|                 videoInfo.audio_streams = new Vector<AudioStream>(); | ||||
|             } | ||||
|             //todo: make this quick and dirty solution a real fallback | ||||
|             // same as the quick and dirty aboth | ||||
|             try { | ||||
|                 videoInfo.audio_streams.addAll( | ||||
|                         DashMpdParser.getAudioStreams(videoInfo.dashMpdUrl, downloader)); | ||||
|             } catch(Exception e) { | ||||
|                 e.printStackTrace(); | ||||
|             } | ||||
|         } | ||||
|         /** Extract video stream url*/ | ||||
|         videoInfo.video_streams = extractor.getVideoStreams(); | ||||
|         /** Extract video only stream url*/ | ||||
|         videoInfo.video_only_streams = extractor.getVideoOnlyStreams(); | ||||
|         videoInfo.uploader_thumbnail_url = extractor.getUploaderThumbnailUrl(); | ||||
|         videoInfo.start_position = extractor.getTimeStamp(); | ||||
|         videoInfo.average_rating = extractor.getAverageRating(); | ||||
|         videoInfo.like_count = extractor.getLikeCount(); | ||||
|         videoInfo.dislike_count = extractor.getDislikeCount(); | ||||
|         videoInfo.next_video = extractor.getNextVideo(); | ||||
|         videoInfo.related_videos = extractor.getRelatedVideos(); | ||||
|  | ||||
|         //Bitmap thumbnail = null; | ||||
|         //Bitmap uploader_thumbnail = null; | ||||
|         //int videoAvailableStatus = VIDEO_AVAILABLE; | ||||
|         return videoInfo; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     public String uploader_thumbnail_url = ""; | ||||
|     public String description = ""; | ||||
|     /*todo: make this lists over vectors*/ | ||||
|     public List<VideoStream> video_streams = null; | ||||
|     public List<AudioStream> audio_streams = null; | ||||
|     public List<VideoStream> video_only_streams = null; | ||||
|     // video streams provided by the dash mpd do not need to be provided as VideoStream. | ||||
|     // Later on this will also aplly to audio streams. Since dash mpd is standarized, | ||||
|     // crawling such a file is not service dependent. Therefore getting audio only streams by yust | ||||
|     // providing the dash mpd fille will be possible in the future. | ||||
|     public String dashMpdUrl = ""; | ||||
|     public int duration = -1; | ||||
|  | ||||
|     /*YouTube-specific fields | ||||
|     todo: move these to a subclass*/ | ||||
|     public int age_limit = 0; | ||||
|     public int like_count = -1; | ||||
|     public int dislike_count = -1; | ||||
|     public String average_rating = ""; | ||||
|     public VideoPreviewInfo next_video = null; | ||||
|     public List<VideoPreviewInfo> related_videos = null; | ||||
|     //in seconds. some metadata is not passed using a VideoInfo object! | ||||
|     public int start_position = 0; | ||||
|     //todo: public int service_id = -1; | ||||
|  | ||||
|     public VideoInfo() {} | ||||
|  | ||||
|     /**Creates a new VideoInfo object from an existing AbstractVideoInfo. | ||||
|      * All the shared properties are copied to the new VideoInfo.*/ | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     public VideoInfo(AbstractVideoInfo avi) { | ||||
|         this.id = avi.id; | ||||
|         this.title = avi.title; | ||||
|         this.uploader = avi.uploader; | ||||
|         this.thumbnail_url = avi.thumbnail_url; | ||||
|         this.thumbnail = avi.thumbnail; | ||||
|         this.webpage_url = avi.webpage_url; | ||||
|         this.upload_date = avi.upload_date; | ||||
|         this.upload_date = avi.upload_date; | ||||
|         this.view_count = avi.view_count; | ||||
|  | ||||
|         //todo: better than this | ||||
|         if(avi instanceof VideoPreviewInfo) { | ||||
|             //shitty String to convert code | ||||
|             String dur = ((VideoPreviewInfo)avi).duration; | ||||
|             int minutes = Integer.parseInt(dur.substring(0, dur.indexOf(":"))); | ||||
|             int seconds = Integer.parseInt(dur.substring(dur.indexOf(":")+1, dur.length())); | ||||
|             this.duration = (minutes*60)+seconds; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static class VideoStream { | ||||
|         //url of the stream | ||||
|         public String url = ""; | ||||
|         public int format = -1; | ||||
|         public String resolution = ""; | ||||
|  | ||||
|         public VideoStream(String url, int format, String res) { | ||||
|             this.url = url; this.format = format; resolution = res; | ||||
|         } | ||||
|  | ||||
|         // reveals wether two streams are the same, but have diferent urls | ||||
|         public boolean equalStats(VideoStream cmp) { | ||||
|             return format == cmp.format | ||||
|                     && resolution == cmp.resolution; | ||||
|         } | ||||
|  | ||||
|         // revelas wether two streams are equal | ||||
|         public boolean equals(VideoStream cmp) { | ||||
|             return equalStats(cmp) | ||||
|                     && url == cmp.url; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @SuppressWarnings("unused") | ||||
|     public static class AudioStream { | ||||
|         public String url = ""; | ||||
|         public int format = -1; | ||||
|         public int bandwidth = -1; | ||||
|         public int sampling_rate = -1; | ||||
|  | ||||
|         public AudioStream(String url, int format, int bandwidth, int samplingRate) { | ||||
|             this.url = url; this.format = format; | ||||
|             this.bandwidth = bandwidth; this.sampling_rate = samplingRate; | ||||
|         } | ||||
|  | ||||
|         // reveals wether two streams are the same, but have diferent urls | ||||
|         public boolean equalStats(AudioStream cmp) { | ||||
|             return format == cmp.format | ||||
|                     && bandwidth == cmp.bandwidth | ||||
|                     && sampling_rate == cmp.sampling_rate; | ||||
|         } | ||||
|  | ||||
|         // revelas wether two streams are equal | ||||
|         public boolean equals(AudioStream cmp) { | ||||
|             return equalStats(cmp) | ||||
|                     && url == cmp.url; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,79 +0,0 @@ | ||||
| package org.schabi.newpipe.crawler; | ||||
|  | ||||
| import android.graphics.Bitmap; | ||||
| import android.os.Parcel; | ||||
| import android.os.Parcelable; | ||||
|  | ||||
| import org.schabi.newpipe.crawler.AbstractVideoInfo; | ||||
|  | ||||
| /** | ||||
|  * Created by Christian Schabesberger on 26.08.15. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org> | ||||
|  * VideoPreviewInfo.java is part of NewPipe. | ||||
|  * | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * NewPipe is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| /**Info object for previews of unopened videos, eg search results, related videos*/ | ||||
| public class VideoPreviewInfo extends AbstractVideoInfo implements Parcelable { | ||||
|     public String duration = ""; | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     protected VideoPreviewInfo(Parcel in) { | ||||
|         id = in.readString(); | ||||
|         title = in.readString(); | ||||
|         uploader = in.readString(); | ||||
|         duration = in.readString(); | ||||
|         thumbnail_url = in.readString(); | ||||
|         thumbnail = (Bitmap) in.readValue(Bitmap.class.getClassLoader()); | ||||
|         webpage_url = in.readString(); | ||||
|         upload_date = in.readString(); | ||||
|         view_count = in.readLong(); | ||||
|     } | ||||
|  | ||||
|     public VideoPreviewInfo() { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int describeContents() { | ||||
|         return 0; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void writeToParcel(Parcel dest, int flags) { | ||||
|         dest.writeString(id); | ||||
|         dest.writeString(title); | ||||
|         dest.writeString(uploader); | ||||
|         dest.writeString(duration); | ||||
|         dest.writeString(thumbnail_url); | ||||
|         dest.writeValue(thumbnail); | ||||
|         dest.writeString(webpage_url); | ||||
|         dest.writeString(upload_date); | ||||
|         dest.writeLong(view_count); | ||||
|     } | ||||
|  | ||||
|     @SuppressWarnings("unused") | ||||
|     public static final Parcelable.Creator<VideoPreviewInfo> CREATOR = new Parcelable.Creator<VideoPreviewInfo>() { | ||||
|         @Override | ||||
|         public VideoPreviewInfo createFromParcel(Parcel in) { | ||||
|             return new VideoPreviewInfo(in); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public VideoPreviewInfo[] newArray(int size) { | ||||
|             return new VideoPreviewInfo[size]; | ||||
|         } | ||||
|     }; | ||||
| } | ||||
| @@ -1,209 +0,0 @@ | ||||
| package org.schabi.newpipe.crawler.services.youtube; | ||||
|  | ||||
| import android.net.Uri; | ||||
| import android.util.Log; | ||||
|  | ||||
| import org.jsoup.Jsoup; | ||||
| import org.jsoup.nodes.Document; | ||||
| import org.jsoup.nodes.Element; | ||||
| import org.schabi.newpipe.crawler.Downloader; | ||||
| import org.schabi.newpipe.crawler.ParsingException; | ||||
| import org.schabi.newpipe.crawler.SearchEngine; | ||||
| import org.schabi.newpipe.crawler.VideoPreviewInfo; | ||||
| import org.w3c.dom.Node; | ||||
| import org.w3c.dom.NodeList; | ||||
| import org.xml.sax.InputSource; | ||||
| import org.xml.sax.SAXException; | ||||
|  | ||||
| import java.io.ByteArrayInputStream; | ||||
| import java.io.IOException; | ||||
| import java.util.ArrayList; | ||||
| import java.util.regex.Matcher; | ||||
| import java.util.regex.Pattern; | ||||
|  | ||||
| import javax.xml.parsers.DocumentBuilder; | ||||
| import javax.xml.parsers.DocumentBuilderFactory; | ||||
| import javax.xml.parsers.ParserConfigurationException; | ||||
|  | ||||
| /** | ||||
|  * Created by Christian Schabesberger on 09.08.15. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org> | ||||
|  * YoutubeSearchEngine.java is part of NewPipe. | ||||
|  * | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * NewPipe is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| public class YoutubeSearchEngine implements SearchEngine { | ||||
|  | ||||
|     private static final String TAG = YoutubeSearchEngine.class.toString(); | ||||
|  | ||||
|     @Override | ||||
|     public Result search(String query, int page, String languageCode, Downloader downloader) | ||||
|             throws IOException, ParsingException { | ||||
|         Result result = new Result(); | ||||
|         Uri.Builder builder = new Uri.Builder(); | ||||
|         builder.scheme("https") | ||||
|                 .authority("www.youtube.com") | ||||
|                 .appendPath("results") | ||||
|                 .appendQueryParameter("search_query", query) | ||||
|                 .appendQueryParameter("page", Integer.toString(page)) | ||||
|                 .appendQueryParameter("filters", "video"); | ||||
|  | ||||
|         String site; | ||||
|         String url = builder.build().toString(); | ||||
|         //if we've been passed a valid language code, append it to the URL | ||||
|         if(!languageCode.isEmpty()) { | ||||
|             //assert Pattern.matches("[a-z]{2}(-([A-Z]{2}|[0-9]{1,3}))?", languageCode); | ||||
|             site  = downloader.download(url, languageCode); | ||||
|         } | ||||
|         else { | ||||
|             site = downloader.download(url); | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|  | ||||
|             Document doc = Jsoup.parse(site, url); | ||||
|             Element list = doc.select("ol[class=\"item-section\"]").first(); | ||||
|  | ||||
|             for (Element item : list.children()) { | ||||
|             /* First we need to determine which kind of item we are working with. | ||||
|                Youtube depicts five different kinds of items on its search result page. These are | ||||
|                regular videos, playlists, channels, two types of video suggestions, and a "no video | ||||
|                found" item. Since we only want videos, we need to filter out all the others. | ||||
|                An example for this can be seen here: | ||||
|                https://www.youtube.com/results?search_query=asdf&page=1 | ||||
|  | ||||
|                We already applied a filter to the url, so we don't need to care about channels and | ||||
|                playlists now. | ||||
|             */ | ||||
|  | ||||
|                 Element el; | ||||
|  | ||||
|                 // both types of spell correction item | ||||
|                 if (!((el = item.select("div[class*=\"spell-correction\"]").first()) == null)) { | ||||
|                     result.suggestion = el.select("a").first().text(); | ||||
|                     // search message item | ||||
|                 } else if (!((el = item.select("div[class*=\"search-message\"]").first()) == null)) { | ||||
|                     result.errorMessage = el.text(); | ||||
|  | ||||
|                     // video item type | ||||
|                 } else if (!((el = item.select("div[class*=\"yt-lockup-video\"").first()) == null)) { | ||||
|                     VideoPreviewInfo resultItem = new VideoPreviewInfo(); | ||||
|                     Element dl = el.select("h3").first().select("a").first(); | ||||
|                     resultItem.webpage_url = dl.attr("abs:href"); | ||||
|                     try { | ||||
|                         Pattern p = Pattern.compile("v=([0-9a-zA-Z-]*)"); | ||||
|                         Matcher m = p.matcher(resultItem.webpage_url); | ||||
|                         resultItem.id = m.group(1); | ||||
|                     } catch (Exception e) { | ||||
|                         //e.printStackTrace(); | ||||
|                     } | ||||
|                     resultItem.title = dl.text(); | ||||
|  | ||||
|                     resultItem.duration = item.select("span[class=\"video-time\"]").first().text(); | ||||
|  | ||||
|                     resultItem.uploader = item.select("div[class=\"yt-lockup-byline\"]").first() | ||||
|                             .select("a").first() | ||||
|                             .text(); | ||||
|                     resultItem.upload_date = item.select("div[class=\"yt-lockup-meta\"]").first() | ||||
|                             .select("li").first() | ||||
|                             .text(); | ||||
|  | ||||
|                     //todo: test against view_count | ||||
|                     String viewCountInfo = item.select("div[class=\"yt-lockup-meta\"]").first() | ||||
|                             .select("li").get(1) | ||||
|                             .text(); | ||||
|                     viewCountInfo = viewCountInfo.substring(0, viewCountInfo.indexOf(' ')); | ||||
|                     viewCountInfo = viewCountInfo.replaceAll("[,.]", ""); | ||||
|                     resultItem.view_count = Long.parseLong(viewCountInfo); | ||||
|  | ||||
|                     Element te = item.select("div[class=\"yt-thumb video-thumb\"]").first() | ||||
|                             .select("img").first(); | ||||
|                     resultItem.thumbnail_url = te.attr("abs:src"); | ||||
|                     // Sometimes youtube sends links to gif files which somehow seem to not exist | ||||
|                     // anymore. Items with such gif also offer a secondary image source. So we are going | ||||
|                     // to use that if we've caught such an item. | ||||
|                     if (resultItem.thumbnail_url.contains(".gif")) { | ||||
|                         resultItem.thumbnail_url = te.attr("abs:data-thumb"); | ||||
|                     } | ||||
|                     result.resultList.add(resultItem); | ||||
|                 } else { | ||||
|                     //noinspection ConstantConditions | ||||
|                     Log.e(TAG, "unexpected element found:\"" + el + "\""); | ||||
|                 } | ||||
|             } | ||||
|         } catch(Exception e) { | ||||
|             throw new ParsingException(e); | ||||
|         } | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public ArrayList<String> suggestionList(String query, Downloader dl) | ||||
|             throws IOException, ParsingException { | ||||
|  | ||||
|         ArrayList<String> suggestions = new ArrayList<>(); | ||||
|  | ||||
|         Uri.Builder builder = new Uri.Builder(); | ||||
|         builder.scheme("https") | ||||
|                 .authority("suggestqueries.google.com") | ||||
|                 .appendPath("complete") | ||||
|                 .appendPath("search") | ||||
|                 .appendQueryParameter("client", "") | ||||
|                 .appendQueryParameter("output", "toolbar") | ||||
|                 .appendQueryParameter("ds", "yt") | ||||
|                 .appendQueryParameter("q", query); | ||||
|         String url = builder.build().toString(); | ||||
|  | ||||
|  | ||||
|         String response = dl.download(url); | ||||
|  | ||||
|         try { | ||||
|  | ||||
|             //TODO: Parse xml data using Jsoup not done | ||||
|             DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); | ||||
|             DocumentBuilder dBuilder; | ||||
|             org.w3c.dom.Document doc = null; | ||||
|  | ||||
|             try { | ||||
|                 dBuilder = dbFactory.newDocumentBuilder(); | ||||
|                 doc = dBuilder.parse(new InputSource( | ||||
|                         new ByteArrayInputStream(response.getBytes("utf-8")))); | ||||
|                 doc.getDocumentElement().normalize(); | ||||
|             } catch (ParserConfigurationException | SAXException | IOException e) { | ||||
|                 e.printStackTrace(); | ||||
|             } | ||||
|  | ||||
|             if (doc != null) { | ||||
|                 NodeList nList = doc.getElementsByTagName("CompleteSuggestion"); | ||||
|                 for (int temp = 0; temp < nList.getLength(); temp++) { | ||||
|  | ||||
|                     NodeList nList1 = doc.getElementsByTagName("suggestion"); | ||||
|                     Node nNode1 = nList1.item(temp); | ||||
|                     if (nNode1.getNodeType() == Node.ELEMENT_NODE) { | ||||
|                         org.w3c.dom.Element eElement = (org.w3c.dom.Element) nNode1; | ||||
|                         suggestions.add(eElement.getAttribute("data")); | ||||
|                     } | ||||
|                 } | ||||
|             } else { | ||||
|                 Log.e(TAG, "GREAT FUCKING ERROR"); | ||||
|             } | ||||
|             return suggestions; | ||||
|         } catch(Exception e) { | ||||
|             throw new ParsingException(e); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| package org.schabi.newpipe.crawler; | ||||
| package org.schabi.newpipe.extractor; | ||||
| 
 | ||||
| import android.graphics.Bitmap; | ||||
| 
 | ||||
| @@ -20,8 +20,19 @@ import android.graphics.Bitmap; | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
| 
 | ||||
| /**Common properties between VideoInfo and VideoPreviewInfo.*/ | ||||
| /**Common properties between StreamInfo and StreamPreviewInfo.*/ | ||||
| public abstract class AbstractVideoInfo { | ||||
|     public static enum StreamType { | ||||
|         NONE,   // placeholder to check if stream type was checked or not | ||||
|         VIDEO_STREAM, | ||||
|         AUDIO_STREAM, | ||||
|         LIVE_STREAM, | ||||
|         AUDIO_LIVE_STREAM, | ||||
|         FILE | ||||
|     } | ||||
| 
 | ||||
|     public StreamType stream_type; | ||||
|     public int service_id = -1; | ||||
|     public String id = ""; | ||||
|     public String title = ""; | ||||
|     public String uploader = ""; | ||||
| @@ -0,0 +1,46 @@ | ||||
| package org.schabi.newpipe.extractor; | ||||
|  | ||||
| /** | ||||
|  * Created by Christian Schabesberger on 04.03.16. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org> | ||||
|  * AudioStream.java is part of NewPipe. | ||||
|  * | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * NewPipe is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| public class AudioStream { | ||||
|     public String url = ""; | ||||
|     public int format = -1; | ||||
|     public int bandwidth = -1; | ||||
|     public int sampling_rate = -1; | ||||
|  | ||||
|     public AudioStream(String url, int format, int bandwidth, int samplingRate) { | ||||
|         this.url = url; this.format = format; | ||||
|         this.bandwidth = bandwidth; this.sampling_rate = samplingRate; | ||||
|     } | ||||
|  | ||||
|     // reveals wether two streams are the same, but have diferent urls | ||||
|     public boolean equalStats(AudioStream cmp) { | ||||
|         return format == cmp.format | ||||
|                 && bandwidth == cmp.bandwidth | ||||
|                 && sampling_rate == cmp.sampling_rate; | ||||
|     } | ||||
|  | ||||
|     // revelas wether two streams are equal | ||||
|     public boolean equals(AudioStream cmp) { | ||||
|         return cmp != null && equalStats(cmp) | ||||
|                 && url == cmp.url; | ||||
|     } | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| package org.schabi.newpipe.crawler; | ||||
| package org.schabi.newpipe.extractor; | ||||
| 
 | ||||
| import android.util.Xml; | ||||
| 
 | ||||
| @@ -31,13 +31,16 @@ import java.util.Vector; | ||||
| 
 | ||||
| public class DashMpdParser { | ||||
| 
 | ||||
|     private DashMpdParser() { | ||||
|     } | ||||
| 
 | ||||
|     static class DashMpdParsingException extends ParsingException { | ||||
|         DashMpdParsingException(String message, Exception e) { | ||||
|             super(message, e); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static List<VideoInfo.AudioStream> getAudioStreams(String dashManifestUrl, | ||||
|     public static List<AudioStream> getAudioStreams(String dashManifestUrl, | ||||
|                                                              Downloader downloader) | ||||
|             throws DashMpdParsingException { | ||||
|         String dashDoc; | ||||
| @@ -46,7 +49,7 @@ public class DashMpdParser { | ||||
|         } catch(IOException ioe) { | ||||
|             throw new DashMpdParsingException("Could not get dash mpd: " + dashManifestUrl, ioe); | ||||
|         } | ||||
|         Vector<VideoInfo.AudioStream> audioStreams = new Vector<>(); | ||||
|         Vector<AudioStream> audioStreams = new Vector<>(); | ||||
|         try { | ||||
|             XmlPullParser parser = Xml.newPullParser(); | ||||
|             parser.setInput(new StringReader(dashDoc)); | ||||
| @@ -83,7 +86,7 @@ public class DashMpdParser { | ||||
|                             } else if(currentMimeType.equals(MediaFormat.M4A.mimeType)) { | ||||
|                                 format = MediaFormat.M4A.id; | ||||
|                             } | ||||
|                             audioStreams.add(new VideoInfo.AudioStream(parser.getText(), | ||||
|                             audioStreams.add(new AudioStream(parser.getText(), | ||||
|                                     format, currentBandwidth, currentSamplingRate)); | ||||
|                         } | ||||
|                         break; | ||||
| @@ -1,4 +1,4 @@ | ||||
| package org.schabi.newpipe.crawler; | ||||
| package org.schabi.newpipe.extractor; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| 
 | ||||
| @@ -1,10 +1,10 @@ | ||||
| package org.schabi.newpipe.crawler; | ||||
| package org.schabi.newpipe.extractor; | ||||
| 
 | ||||
| /** | ||||
|  * Created by Christian Schabesberger on 30.01.16. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org> | ||||
|  * CrawlingException.java is part of NewPipe. | ||||
|  * ExtractionException.java is part of NewPipe. | ||||
|  * | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
| @@ -20,18 +20,16 @@ package org.schabi.newpipe.crawler; | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
| 
 | ||||
| public class CrawlingException extends Exception { | ||||
|     public CrawlingException() {} | ||||
| 
 | ||||
|     public CrawlingException(String message) { | ||||
| public class ExtractionException extends Exception { | ||||
|     public ExtractionException(String message) { | ||||
|         super(message); | ||||
|     } | ||||
| 
 | ||||
|     public CrawlingException(Throwable cause) { | ||||
|     public ExtractionException(Throwable cause) { | ||||
|         super(cause); | ||||
|     } | ||||
| 
 | ||||
|     public CrawlingException(String message, Throwable cause) { | ||||
|     public ExtractionException(String message, Throwable cause) { | ||||
|         super(message, cause); | ||||
|     } | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| package org.schabi.newpipe.crawler; | ||||
| package org.schabi.newpipe.extractor; | ||||
| 
 | ||||
| /** | ||||
|  * Created by Adam Howard on 08/11/15. | ||||
| @@ -1,4 +1,6 @@ | ||||
| package org.schabi.newpipe.crawler; | ||||
| package org.schabi.newpipe.extractor; | ||||
| 
 | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import java.io.UnsupportedEncodingException; | ||||
| import java.net.URLDecoder; | ||||
| @@ -30,6 +32,9 @@ import java.util.regex.Pattern; | ||||
| /** avoid using regex !!! */ | ||||
| public class Parser { | ||||
| 
 | ||||
|     private Parser() { | ||||
|     } | ||||
| 
 | ||||
|     public static class RegexException extends ParsingException { | ||||
|         public RegexException(String message) { | ||||
|             super(message); | ||||
| @@ -52,8 +57,12 @@ public class Parser { | ||||
|     public static Map<String, String> compatParseMap(final String input) throws UnsupportedEncodingException { | ||||
|         Map<String, String> map = new HashMap<>(); | ||||
|         for(String arg : input.split("&")) { | ||||
|             String[] split_arg = arg.split("="); | ||||
|             map.put(split_arg[0], URLDecoder.decode(split_arg[1], "UTF-8")); | ||||
|             String[] splitArg = arg.split("="); | ||||
|             if(splitArg.length > 1) { | ||||
|                 map.put(splitArg[0], URLDecoder.decode(splitArg[1], "UTF-8")); | ||||
|             } else { | ||||
|                 map.put(splitArg[0], ""); | ||||
|             } | ||||
|         } | ||||
|         return map; | ||||
|     } | ||||
| @@ -1,4 +1,4 @@ | ||||
| package org.schabi.newpipe.crawler; | ||||
| package org.schabi.newpipe.extractor; | ||||
| 
 | ||||
| /** | ||||
|  * Created by Christian Schabesberger on 31.01.16. | ||||
| @@ -21,14 +21,10 @@ package org.schabi.newpipe.crawler; | ||||
|  */ | ||||
| 
 | ||||
| 
 | ||||
| public class ParsingException extends CrawlingException { | ||||
|     public ParsingException() {} | ||||
| public class ParsingException extends ExtractionException { | ||||
|     public ParsingException(String message) { | ||||
|         super(message); | ||||
|     } | ||||
|     public ParsingException(Throwable cause) { | ||||
|         super(cause); | ||||
|     } | ||||
|     public ParsingException(String message, Throwable cause) { | ||||
|         super(message, cause); | ||||
|     } | ||||
| @@ -1,4 +1,4 @@ | ||||
| package org.schabi.newpipe.crawler; | ||||
| package org.schabi.newpipe.extractor; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| import java.util.ArrayList; | ||||
| @@ -26,17 +26,29 @@ import java.util.Vector; | ||||
|  */ | ||||
| 
 | ||||
| @SuppressWarnings("ALL") | ||||
| public interface SearchEngine { | ||||
|     class Result { | ||||
|         public String errorMessage = ""; | ||||
|         public String suggestion = ""; | ||||
|         public final List<VideoPreviewInfo> resultList = new Vector<>(); | ||||
| public abstract class SearchEngine { | ||||
|     public static class NothingFoundException extends ExtractionException { | ||||
|         public NothingFoundException(String message) { | ||||
|             super(message); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     ArrayList<String> suggestionList(String query, Downloader dl) | ||||
|             throws CrawlingException, IOException; | ||||
|     private StreamPreviewInfoCollector collector; | ||||
| 
 | ||||
|     public SearchEngine(StreamUrlIdHandler urlIdHandler, int serviceId) { | ||||
|         collector = new StreamPreviewInfoCollector(urlIdHandler, serviceId); | ||||
|     } | ||||
| 
 | ||||
|     public StreamPreviewInfoCollector getStreamPreviewInfoCollector() { | ||||
|         return collector; | ||||
|     } | ||||
| 
 | ||||
|     public abstract ArrayList<String> suggestionList( | ||||
|             String query,String contentCountry, Downloader dl) | ||||
|             throws ExtractionException, IOException; | ||||
| 
 | ||||
|     //Result search(String query, int page); | ||||
|     Result search(String query, int page, String contentCountry, Downloader dl) | ||||
|             throws CrawlingException, IOException; | ||||
|     public abstract StreamPreviewInfoCollector search( | ||||
|             String query, int page, String contentCountry, Downloader dl) | ||||
|             throws ExtractionException, IOException; | ||||
| } | ||||
| @@ -0,0 +1,47 @@ | ||||
| package org.schabi.newpipe.extractor; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.util.List; | ||||
| import java.util.Vector; | ||||
|  | ||||
| /** | ||||
|  * Created by Christian Schabesberger on 29.02.16. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org> | ||||
|  * SearchResult.java is part of NewPipe. | ||||
|  * | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * NewPipe is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| public class SearchResult { | ||||
|     public static SearchResult getSearchResult(SearchEngine engine, String query, | ||||
|                                                int page, String languageCode, Downloader dl) | ||||
|             throws ExtractionException, IOException { | ||||
|  | ||||
|         SearchResult result = engine.search(query, page, languageCode, dl).getSearchResult(); | ||||
|         if(result.resultList.isEmpty()) { | ||||
|             if(result.suggestion.isEmpty()) { | ||||
|                 throw new ExtractionException("Empty result despite no error"); | ||||
|             } else { | ||||
|                 // This is used as a fallback. Do not relay on it !!! | ||||
|                 throw new SearchEngine.NothingFoundException(result.suggestion); | ||||
|             } | ||||
|         } | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
|     public String suggestion = ""; | ||||
|     public final List<StreamPreviewInfo> resultList = new Vector<>(); | ||||
|     public List<Exception> errors = new Vector<>(); | ||||
| } | ||||
| @@ -1,8 +1,8 @@ | ||||
| package org.schabi.newpipe.crawler; | ||||
| package org.schabi.newpipe.extractor; | ||||
| 
 | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import org.schabi.newpipe.crawler.services.youtube.YoutubeService; | ||||
| import org.schabi.newpipe.extractor.services.youtube.YoutubeService; | ||||
| 
 | ||||
| /** | ||||
|  * Created by Christian Schabesberger on 23.08.15. | ||||
| @@ -29,26 +29,43 @@ import org.schabi.newpipe.crawler.services.youtube.YoutubeService; | ||||
| 
 | ||||
| @SuppressWarnings("ALL") | ||||
| public class ServiceList { | ||||
| 
 | ||||
|     private ServiceList() { | ||||
|     } | ||||
| 
 | ||||
|     private static final String TAG = ServiceList.class.toString(); | ||||
|     private static final StreamingService[] services = { | ||||
|         new YoutubeService() | ||||
|         new YoutubeService(0) | ||||
|     }; | ||||
|     public static StreamingService[] getServices() { | ||||
|         return services; | ||||
|     } | ||||
|     public static StreamingService getService(int serviceId) { | ||||
|         return services[serviceId]; | ||||
|     public static StreamingService getService(int serviceId) throws ExtractionException { | ||||
|         for(StreamingService s : services) { | ||||
|             if(s.getServiceId() == serviceId) { | ||||
|                 return s; | ||||
|             } | ||||
|     public static StreamingService getService(String serviceName) { | ||||
|         } | ||||
|         throw new ExtractionException("Service not known: " + Integer.toString(serviceId)); | ||||
|     } | ||||
|     public static StreamingService getService(String serviceName) throws ExtractionException { | ||||
|         return services[getIdOfService(serviceName)]; | ||||
|     } | ||||
|     public static int getIdOfService(String serviceName) { | ||||
|     public static String getNameOfService(int id) { | ||||
|         try { | ||||
|             return getService(id).getServiceInfo().name; | ||||
|         } catch (Exception e) { | ||||
|             System.err.println("Service id not known"); | ||||
|             e.printStackTrace(); | ||||
|             return ""; | ||||
|         } | ||||
|     } | ||||
|     public static int getIdOfService(String serviceName) throws ExtractionException { | ||||
|         for(int i = 0; i < services.length; i++) { | ||||
|             if(services[i].getServiceInfo().name.equals(serviceName)) { | ||||
|                 return i; | ||||
|             } | ||||
|         } | ||||
|         Log.e(TAG, "Error: Service " + serviceName + " not known."); | ||||
|         return -1; | ||||
|         throw new ExtractionException("Error: Service " + serviceName + " not known."); | ||||
|     } | ||||
| } | ||||
| @@ -1,9 +1,9 @@ | ||||
| package org.schabi.newpipe.crawler; | ||||
| package org.schabi.newpipe.extractor; | ||||
| 
 | ||||
| /** | ||||
|  * Created by Christian Schabesberger on 10.08.15. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org> | ||||
|  * Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org> | ||||
|  * StreamExtractor.java is part of NewPipe. | ||||
|  * | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
| @@ -26,10 +26,11 @@ import java.util.List; | ||||
| 
 | ||||
| 
 | ||||
| @SuppressWarnings("ALL") | ||||
| public interface StreamExtractor { | ||||
| public abstract class StreamExtractor { | ||||
| 
 | ||||
|     public class ExctractorInitException extends CrawlingException { | ||||
|         public ExctractorInitException() {} | ||||
|     private int serviceId; | ||||
| 
 | ||||
|     public class ExctractorInitException extends ExtractionException { | ||||
|         public ExctractorInitException(String message) { | ||||
|             super(message); | ||||
|         } | ||||
| @@ -42,37 +43,41 @@ public interface StreamExtractor { | ||||
|     } | ||||
| 
 | ||||
|     public class ContentNotAvailableException extends ParsingException { | ||||
|         public ContentNotAvailableException() {} | ||||
|         public ContentNotAvailableException(String message) { | ||||
|             super(message); | ||||
|         } | ||||
|         public ContentNotAvailableException(Throwable cause) { | ||||
|             super(cause); | ||||
|         } | ||||
|         public ContentNotAvailableException(String message, Throwable cause) { | ||||
|             super(message, cause); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public StreamExtractor(String url, Downloader dl, int serviceId) { | ||||
|         this.serviceId = serviceId; | ||||
|     } | ||||
| 
 | ||||
|     public abstract int getTimeStamp() throws ParsingException; | ||||
|     public abstract String getTitle() throws ParsingException; | ||||
|     public abstract String getDescription() throws ParsingException; | ||||
|     public abstract String getUploader() throws ParsingException; | ||||
|     public abstract int getLength() throws ParsingException; | ||||
|     public abstract long getViews() throws ParsingException; | ||||
|     public abstract long getViewCount() throws ParsingException; | ||||
|     public abstract String getUploadDate() throws ParsingException; | ||||
|     public abstract String getThumbnailUrl() throws ParsingException; | ||||
|     public abstract String getUploaderThumbnailUrl() throws ParsingException; | ||||
|     public abstract List<VideoInfo.AudioStream> getAudioStreams() throws ParsingException; | ||||
|     public abstract List<VideoInfo.VideoStream> getVideoStreams() throws ParsingException; | ||||
|     public abstract List<VideoInfo.VideoStream> getVideoOnlyStreams() throws ParsingException; | ||||
|     public abstract List<AudioStream> getAudioStreams() throws ParsingException; | ||||
|     public abstract List<VideoStream> getVideoStreams() throws ParsingException; | ||||
|     public abstract List<VideoStream> getVideoOnlyStreams() throws ParsingException; | ||||
|     public abstract String getDashMpdUrl() throws ParsingException; | ||||
|     public abstract int getAgeLimit() throws ParsingException; | ||||
|     public abstract String getAverageRating() throws ParsingException; | ||||
|     public abstract int getLikeCount() throws ParsingException; | ||||
|     public abstract int getDislikeCount() throws ParsingException; | ||||
|     public abstract VideoPreviewInfo getNextVideo() throws ParsingException; | ||||
|     public abstract List<VideoPreviewInfo> getRelatedVideos() throws ParsingException; | ||||
|     public abstract VideoUrlIdHandler getUrlIdConverter(); | ||||
|     public abstract StreamPreviewInfo getNextVideo() throws ParsingException; | ||||
|     public abstract List<StreamPreviewInfo> getRelatedVideos() throws ParsingException; | ||||
|     public abstract StreamUrlIdHandler getUrlIdConverter(); | ||||
|     public abstract String getPageUrl(); | ||||
|     public abstract StreamInfo.StreamType getStreamType() throws ParsingException; | ||||
|     public int getServiceId() { | ||||
|         return serviceId; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										268
									
								
								app/src/main/java/org/schabi/newpipe/extractor/StreamInfo.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,268 @@ | ||||
| package org.schabi.newpipe.extractor; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.util.List; | ||||
| import java.util.Vector; | ||||
|  | ||||
| /** | ||||
|  * Created by Christian Schabesberger on 26.08.15. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org> | ||||
|  * StreamInfo.java is part of NewPipe. | ||||
|  * | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * NewPipe is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| /**Info object for opened videos, ie the video ready to play.*/ | ||||
| @SuppressWarnings("ALL") | ||||
| public class StreamInfo extends AbstractVideoInfo { | ||||
|  | ||||
|     public static class StreamExctractException extends ExtractionException { | ||||
|         StreamExctractException(String message) { | ||||
|             super(message); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public StreamInfo() {} | ||||
|  | ||||
|     /**Creates a new StreamInfo object from an existing AbstractVideoInfo. | ||||
|      * All the shared properties are copied to the new StreamInfo.*/ | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     public StreamInfo(AbstractVideoInfo avi) { | ||||
|         this.id = avi.id; | ||||
|         this.title = avi.title; | ||||
|         this.uploader = avi.uploader; | ||||
|         this.thumbnail_url = avi.thumbnail_url; | ||||
|         this.thumbnail = avi.thumbnail; | ||||
|         this.webpage_url = avi.webpage_url; | ||||
|         this.upload_date = avi.upload_date; | ||||
|         this.upload_date = avi.upload_date; | ||||
|         this.view_count = avi.view_count; | ||||
|  | ||||
|         //todo: better than this | ||||
|         if(avi instanceof StreamPreviewInfo) { | ||||
|             //shitty String to convert code | ||||
|             /* | ||||
|             String dur = ((StreamPreviewInfo)avi).duration; | ||||
|             int minutes = Integer.parseInt(dur.substring(0, dur.indexOf(":"))); | ||||
|             int seconds = Integer.parseInt(dur.substring(dur.indexOf(":")+1, dur.length())); | ||||
|             */ | ||||
|             this.duration = ((StreamPreviewInfo)avi).duration; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void addException(Exception e) { | ||||
|         errors.add(e); | ||||
|     } | ||||
|  | ||||
|     /**Fills out the video info fields which are common to all services. | ||||
|      * Probably needs to be overridden by subclasses*/ | ||||
|     public static StreamInfo getVideoInfo(StreamExtractor extractor, Downloader downloader) | ||||
|             throws ExtractionException, IOException { | ||||
|         StreamInfo streamInfo = new StreamInfo(); | ||||
|  | ||||
|         streamInfo = extractImportantData(streamInfo, extractor, downloader); | ||||
|         streamInfo = extractStreams(streamInfo, extractor, downloader); | ||||
|         streamInfo = extractOptionalData(streamInfo, extractor, downloader); | ||||
|  | ||||
|         return streamInfo; | ||||
|     } | ||||
|  | ||||
|     private static StreamInfo extractImportantData( | ||||
|             StreamInfo streamInfo, StreamExtractor extractor, Downloader downloader) | ||||
|             throws ExtractionException, IOException { | ||||
|         /* ---- importand data, withoug the video can't be displayed goes here: ---- */ | ||||
|         // if one of these is not available an exception is ment to be thrown directly into the frontend. | ||||
|  | ||||
|         StreamUrlIdHandler uiconv = extractor.getUrlIdConverter(); | ||||
|  | ||||
|         streamInfo.service_id = extractor.getServiceId(); | ||||
|         streamInfo.webpage_url = extractor.getPageUrl(); | ||||
|         streamInfo.stream_type = extractor.getStreamType(); | ||||
|         streamInfo.id = uiconv.getVideoId(extractor.getPageUrl()); | ||||
|         streamInfo.title = extractor.getTitle(); | ||||
|         streamInfo.age_limit = extractor.getAgeLimit(); | ||||
|  | ||||
|         if((streamInfo.stream_type == StreamType.NONE) | ||||
|                 || (streamInfo.webpage_url == null || streamInfo.webpage_url.isEmpty()) | ||||
|                 || (streamInfo.id == null || streamInfo.id.isEmpty()) | ||||
|                 || (streamInfo.title == null /* streamInfo.title can be empty of course */) | ||||
|                 || (streamInfo.age_limit == -1)) { | ||||
|             throw new ExtractionException("Some importand stream information was not given."); | ||||
|         } | ||||
|  | ||||
|         return streamInfo; | ||||
|     } | ||||
|  | ||||
|     private static StreamInfo extractStreams( | ||||
|             StreamInfo streamInfo, StreamExtractor extractor, Downloader downloader) | ||||
|             throws ExtractionException, IOException { | ||||
|         /* ---- stream extraction goes here ---- */ | ||||
|         // At least one type of stream has to be available, | ||||
|         // otherwise an exception will be thrown directly into the frontend. | ||||
|  | ||||
|         try { | ||||
|             streamInfo.dashMpdUrl = extractor.getDashMpdUrl(); | ||||
|         } catch(Exception e) { | ||||
|             streamInfo.addException(new ExtractionException("Couldn't get Dash manifest", e)); | ||||
|         } | ||||
|  | ||||
|         /*  Load and extract audio */ | ||||
|         try { | ||||
|             streamInfo.audio_streams = extractor.getAudioStreams(); | ||||
|         } catch(Exception e) { | ||||
|             streamInfo.addException(new ExtractionException("Couldn't get audio streams", e)); | ||||
|         } | ||||
|         // also try to get streams from the dashMpd | ||||
|         if(streamInfo.dashMpdUrl != null && !streamInfo.dashMpdUrl.isEmpty()) { | ||||
|             if(streamInfo.audio_streams == null) { | ||||
|                 streamInfo.audio_streams = new Vector<>(); | ||||
|             } | ||||
|             //todo: make this quick and dirty solution a real fallback | ||||
|             // same as the quick and dirty aboth | ||||
|             try { | ||||
|                 streamInfo.audio_streams.addAll( | ||||
|                         DashMpdParser.getAudioStreams(streamInfo.dashMpdUrl, downloader)); | ||||
|             } catch(Exception e) { | ||||
|                 streamInfo.addException( | ||||
|                         new ExtractionException("Couldn't get audio streams from dash mpd", e)); | ||||
|             } | ||||
|         } | ||||
|         /* Extract video stream url*/ | ||||
|         try { | ||||
|             streamInfo.video_streams = extractor.getVideoStreams(); | ||||
|         } catch (Exception e) { | ||||
|             streamInfo.addException( | ||||
|                     new ExtractionException("Couldn't get video streams", e)); | ||||
|         } | ||||
|         /* Extract video only stream url*/ | ||||
|         try { | ||||
|             streamInfo.video_only_streams = extractor.getVideoOnlyStreams(); | ||||
|         } catch(Exception e) { | ||||
|             streamInfo.addException( | ||||
|                     new ExtractionException("Couldn't get video only streams", e)); | ||||
|         } | ||||
|  | ||||
|         // either dash_mpd audio_only or video has to be available, otherwise we didn't get a stream, | ||||
|         // and therefore failed. (Since video_only_streams are just optional they don't caunt). | ||||
|         if((streamInfo.video_streams == null || streamInfo.video_streams.isEmpty()) | ||||
|                 && (streamInfo.audio_streams == null || streamInfo.audio_streams.isEmpty()) | ||||
|                 && (streamInfo.dashMpdUrl == null || streamInfo.dashMpdUrl.isEmpty())) { | ||||
|             throw new StreamExctractException( | ||||
|                     "Could not get any stream. See error variable to get further details."); | ||||
|         } | ||||
|  | ||||
|         return streamInfo; | ||||
|     } | ||||
|  | ||||
|     private static StreamInfo extractOptionalData( | ||||
|             StreamInfo streamInfo, StreamExtractor extractor, Downloader downloader) { | ||||
|         /*  ---- optional data goes here: ---- */ | ||||
|         // If one of these failes, the frontend neets to handle that they are not available. | ||||
|         // Exceptions are therfore not thrown into the frontend, but stored into the error List, | ||||
|         // so the frontend can afterwads check where errors happend. | ||||
|  | ||||
|         try { | ||||
|             streamInfo.thumbnail_url = extractor.getThumbnailUrl(); | ||||
|         } catch(Exception e) { | ||||
|             streamInfo.addException(e); | ||||
|         } | ||||
|         try { | ||||
|             streamInfo.duration = extractor.getLength(); | ||||
|         } catch(Exception e) { | ||||
|             streamInfo.addException(e); | ||||
|         } | ||||
|         try { | ||||
|             streamInfo.uploader = extractor.getUploader(); | ||||
|         } catch(Exception e) { | ||||
|             streamInfo.addException(e); | ||||
|         } | ||||
|         try { | ||||
|             streamInfo.description = extractor.getDescription(); | ||||
|         } catch(Exception e) { | ||||
|             streamInfo.addException(e); | ||||
|         } | ||||
|         try { | ||||
|             streamInfo.view_count = extractor.getViewCount(); | ||||
|         } catch(Exception e) { | ||||
|             streamInfo.addException(e); | ||||
|         } | ||||
|         try { | ||||
|             streamInfo.upload_date = extractor.getUploadDate(); | ||||
|         } catch(Exception e) { | ||||
|             streamInfo.addException(e); | ||||
|         } | ||||
|         try { | ||||
|             streamInfo.uploader_thumbnail_url = extractor.getUploaderThumbnailUrl(); | ||||
|         } catch(Exception e) { | ||||
|             streamInfo.addException(e); | ||||
|         } | ||||
|         try { | ||||
|             streamInfo.start_position = extractor.getTimeStamp(); | ||||
|         } catch(Exception e) { | ||||
|             streamInfo.addException(e); | ||||
|         } | ||||
|         try { | ||||
|             streamInfo.average_rating = extractor.getAverageRating(); | ||||
|         } catch(Exception e) { | ||||
|             streamInfo.addException(e); | ||||
|         } | ||||
|         try { | ||||
|             streamInfo.like_count = extractor.getLikeCount(); | ||||
|         } catch(Exception e) { | ||||
|             streamInfo.addException(e); | ||||
|         } | ||||
|         try { | ||||
|             streamInfo.dislike_count = extractor.getDislikeCount(); | ||||
|         } catch(Exception e) { | ||||
|             streamInfo.addException(e); | ||||
|         } | ||||
|         try { | ||||
|             streamInfo.next_video = extractor.getNextVideo(); | ||||
|         } catch(Exception e) { | ||||
|             streamInfo.addException(e); | ||||
|         } | ||||
|         try { | ||||
|             streamInfo.related_videos = extractor.getRelatedVideos(); | ||||
|         } catch(Exception e) { | ||||
|             streamInfo.addException(e); | ||||
|         } | ||||
|  | ||||
|         return streamInfo; | ||||
|     } | ||||
|  | ||||
|     public String uploader_thumbnail_url = ""; | ||||
|     public String description = ""; | ||||
|  | ||||
|     public List<VideoStream> video_streams = null; | ||||
|     public List<AudioStream> audio_streams = null; | ||||
|     public List<VideoStream> video_only_streams = null; | ||||
|     // video streams provided by the dash mpd do not need to be provided as VideoStream. | ||||
|     // Later on this will also aplly to audio streams. Since dash mpd is standarized, | ||||
|     // crawling such a file is not service dependent. Therefore getting audio only streams by yust | ||||
|     // providing the dash mpd fille will be possible in the future. | ||||
|     public String dashMpdUrl = ""; | ||||
|     public int duration = -1; | ||||
|  | ||||
|     public int age_limit = -1; | ||||
|     public int like_count = -1; | ||||
|     public int dislike_count = -1; | ||||
|     public String average_rating = ""; | ||||
|     public StreamPreviewInfo next_video = null; | ||||
|     public List<StreamPreviewInfo> related_videos = null; | ||||
|     //in seconds. some metadata is not passed using a StreamInfo object! | ||||
|     public int start_position = 0; | ||||
|  | ||||
|     public List<Exception> errors = new Vector<>(); | ||||
| } | ||||
| @@ -0,0 +1,26 @@ | ||||
| package org.schabi.newpipe.extractor; | ||||
|  | ||||
| /** | ||||
|  * Created by Christian Schabesberger on 26.08.15. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org> | ||||
|  * StreamPreviewInfo.java is part of NewPipe. | ||||
|  * | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * NewPipe is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| /**Info object for previews of unopened videos, eg search results, related videos*/ | ||||
| public class StreamPreviewInfo extends AbstractVideoInfo { | ||||
|     public int duration = 0; | ||||
| } | ||||
| @@ -0,0 +1,94 @@ | ||||
| package org.schabi.newpipe.extractor; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamUrlIdHandler; | ||||
|  | ||||
| /** | ||||
|  * Created by Christian Schabesberger on 28.02.16. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org> | ||||
|  * StreamPreviewInfoCollector.java is part of NewPipe. | ||||
|  * | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * NewPipe is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| public class StreamPreviewInfoCollector { | ||||
|     private SearchResult result = new SearchResult(); | ||||
|     private StreamUrlIdHandler urlIdHandler = null; | ||||
|     private int serviceId = -1; | ||||
|  | ||||
|     public StreamPreviewInfoCollector(StreamUrlIdHandler handler, int serviceId) { | ||||
|         urlIdHandler = handler; | ||||
|         this.serviceId = serviceId; | ||||
|     } | ||||
|  | ||||
|     public void setSuggestion(String suggestion) { | ||||
|         result.suggestion = suggestion; | ||||
|     } | ||||
|  | ||||
|     public void addError(Exception e) { | ||||
|         result.errors.add(e); | ||||
|     } | ||||
|  | ||||
|     public SearchResult getSearchResult() { | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
|     public void commit(StreamPreviewInfoExtractor extractor) throws ParsingException { | ||||
|         try { | ||||
|             StreamPreviewInfo resultItem = new StreamPreviewInfo(); | ||||
|             // importand information | ||||
|             resultItem.service_id = serviceId; | ||||
|             resultItem.webpage_url = extractor.getWebPageUrl(); | ||||
|             if (urlIdHandler == null) { | ||||
|                 throw new ParsingException("Error: UrlIdHandler not set"); | ||||
|             } else { | ||||
|                 resultItem.id = (new YoutubeStreamUrlIdHandler()).getVideoId(resultItem.webpage_url); | ||||
|             } | ||||
|             resultItem.title = extractor.getTitle(); | ||||
|             resultItem.stream_type = extractor.getStreamType(); | ||||
|  | ||||
|             // optional iformation | ||||
|             try { | ||||
|                 resultItem.duration = extractor.getDuration(); | ||||
|             } catch (Exception e) { | ||||
|                 addError(e); | ||||
|             } | ||||
|             try { | ||||
|                 resultItem.uploader = extractor.getUploader(); | ||||
|             } catch (Exception e) { | ||||
|                 addError(e); | ||||
|             } | ||||
|             try { | ||||
|                 resultItem.upload_date = extractor.getUploadDate(); | ||||
|             } catch (Exception e) { | ||||
|                 addError(e); | ||||
|             } | ||||
|             try { | ||||
|                 resultItem.view_count = extractor.getViewCount(); | ||||
|             } catch (Exception e) { | ||||
|                 addError(e); | ||||
|             } | ||||
|             try { | ||||
|                 resultItem.thumbnail_url = extractor.getThumbnailUrl(); | ||||
|             } catch (Exception e) { | ||||
|                 addError(e); | ||||
|             } | ||||
|  | ||||
|             result.resultList.add(resultItem); | ||||
|         } catch (Exception e) { | ||||
|             addError(e); | ||||
|         } | ||||
|  | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,32 @@ | ||||
| package org.schabi.newpipe.extractor; | ||||
|  | ||||
| /** | ||||
|  * Created by Christian Schabesberger on 28.02.16. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org> | ||||
|  * StreamPreviewInfoExtractor.java is part of NewPipe. | ||||
|  * | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * NewPipe is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| public interface StreamPreviewInfoExtractor { | ||||
|     AbstractVideoInfo.StreamType getStreamType() throws ParsingException; | ||||
|     String getWebPageUrl() throws ParsingException; | ||||
|     String getTitle() throws ParsingException; | ||||
|     int getDuration() throws ParsingException; | ||||
|     String getUploader() throws ParsingException; | ||||
|     String getUploadDate() throws ParsingException; | ||||
|     long getViewCount() throws  ParsingException; | ||||
|     String getThumbnailUrl() throws  ParsingException; | ||||
| } | ||||
| @@ -1,10 +1,10 @@ | ||||
| package org.schabi.newpipe.crawler; | ||||
| package org.schabi.newpipe.extractor; | ||||
| 
 | ||||
| /** | ||||
|  * Created by Christian Schabesberger on 02.02.16. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org> | ||||
|  * VideoUrlIdHandler.java is part of NewPipe. | ||||
|  * StreamUrlIdHandler.java is part of NewPipe. | ||||
|  * | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
| @@ -20,7 +20,7 @@ package org.schabi.newpipe.crawler; | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
| 
 | ||||
| public interface VideoUrlIdHandler { | ||||
| public interface StreamUrlIdHandler { | ||||
|     String getVideoUrl(String videoId); | ||||
|     String getVideoId(String siteUrl) throws ParsingException; | ||||
|     String cleanUrl(String siteUrl) throws ParsingException; | ||||
| @@ -1,11 +1,11 @@ | ||||
| package org.schabi.newpipe.crawler; | ||||
| package org.schabi.newpipe.extractor; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| 
 | ||||
| /** | ||||
|  * Created by Christian Schabesberger on 23.08.15. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org> | ||||
|  * Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org> | ||||
|  * StreamingService.java is part of NewPipe. | ||||
|  * | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
| @@ -22,16 +22,25 @@ import java.io.IOException; | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
| 
 | ||||
| public interface StreamingService { | ||||
|     class ServiceInfo { | ||||
| public abstract class StreamingService { | ||||
|     public class ServiceInfo { | ||||
|         public String name = ""; | ||||
|     } | ||||
|     ServiceInfo getServiceInfo(); | ||||
|     StreamExtractor getExtractorInstance(String url, Downloader downloader) | ||||
|             throws IOException, CrawlingException; | ||||
|     SearchEngine getSearchEngineInstance(); | ||||
| 
 | ||||
|     VideoUrlIdHandler getUrlIdHandler(); | ||||
| 
 | ||||
|     private int serviceId; | ||||
| 
 | ||||
|     public StreamingService(int id) { | ||||
|         serviceId = id; | ||||
|     } | ||||
| 
 | ||||
|     public abstract ServiceInfo getServiceInfo(); | ||||
| 
 | ||||
|     public abstract StreamExtractor getExtractorInstance(String url, Downloader downloader) | ||||
|             throws IOException, ExtractionException; | ||||
|     public abstract SearchEngine getSearchEngineInstance(Downloader downloader); | ||||
|     public abstract StreamUrlIdHandler getUrlIdHandlerInstance(); | ||||
| 
 | ||||
|     public final int getServiceId() { | ||||
|         return serviceId; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,44 @@ | ||||
| package org.schabi.newpipe.extractor; | ||||
|  | ||||
| /** | ||||
|  * Created by Christian Schabesberger on 04.03.16. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org> | ||||
|  * VideoStream.java is part of NewPipe. | ||||
|  * | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * NewPipe is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| public class VideoStream { | ||||
|     //url of the stream | ||||
|     public String url = ""; | ||||
|     public int format = -1; | ||||
|     public String resolution = ""; | ||||
|  | ||||
|     public VideoStream(String url, int format, String res) { | ||||
|         this.url = url; this.format = format; resolution = res; | ||||
|     } | ||||
|  | ||||
|     // reveals wether two streams are the same, but have diferent urls | ||||
|     public boolean equalStats(VideoStream cmp) { | ||||
|         return format == cmp.format | ||||
|                 && resolution == cmp.resolution; | ||||
|     } | ||||
|  | ||||
|     // revelas wether two streams are equal | ||||
|     public boolean equals(VideoStream cmp) { | ||||
|         return cmp != null && equalStats(cmp) | ||||
|                 && url == cmp.url; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,65 @@ | ||||
| package org.schabi.newpipe.extractor.services.youtube; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.ParsingException; | ||||
|  | ||||
| /** | ||||
|  * Created by Christian Schabesberger on 02.03.16. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org> | ||||
|  * YoutubeParsingHelper.java is part of NewPipe. | ||||
|  * | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * NewPipe is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| public class YoutubeParsingHelper { | ||||
|  | ||||
|     private YoutubeParsingHelper() { | ||||
|     } | ||||
|  | ||||
|     public static int parseDurationString(String input) | ||||
|             throws ParsingException, NumberFormatException { | ||||
|         String[] splitInput = input.split(":"); | ||||
|         String days = "0"; | ||||
|         String hours = "0"; | ||||
|         String minutes = "0"; | ||||
|         String seconds; | ||||
|  | ||||
|         switch(splitInput.length) { | ||||
|             case 4: | ||||
|                 days = splitInput[0]; | ||||
|                 hours = splitInput[1]; | ||||
|                 minutes = splitInput[2]; | ||||
|                 seconds = splitInput[3]; | ||||
|                 break; | ||||
|             case 3: | ||||
|                 hours = splitInput[0]; | ||||
|                 minutes = splitInput[1]; | ||||
|                 seconds = splitInput[2]; | ||||
|                 break; | ||||
|             case 2: | ||||
|                 minutes = splitInput[0]; | ||||
|                 seconds = splitInput[1]; | ||||
|                 break; | ||||
|             case 1: | ||||
|                 seconds = splitInput[0]; | ||||
|                 break; | ||||
|             default: | ||||
|                 throw new ParsingException("Error duration string with unknown format: " + input); | ||||
|         } | ||||
|         return ((((Integer.parseInt(days) * 24) | ||||
|                 + Integer.parseInt(hours) * 60) | ||||
|                 + Integer.parseInt(minutes)) * 60) | ||||
|                 + Integer.parseInt(seconds); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,193 @@ | ||||
| package org.schabi.newpipe.extractor.services.youtube; | ||||
|  | ||||
| import org.jsoup.Jsoup; | ||||
| import org.jsoup.nodes.Document; | ||||
| import org.jsoup.nodes.Element; | ||||
| import org.schabi.newpipe.extractor.AbstractVideoInfo; | ||||
| import org.schabi.newpipe.extractor.Downloader; | ||||
| import org.schabi.newpipe.extractor.ExtractionException; | ||||
| import org.schabi.newpipe.extractor.Parser; | ||||
| import org.schabi.newpipe.extractor.ParsingException; | ||||
| import org.schabi.newpipe.extractor.SearchEngine; | ||||
| import org.schabi.newpipe.extractor.StreamPreviewInfoCollector; | ||||
| import org.schabi.newpipe.extractor.StreamPreviewInfoExtractor; | ||||
| import org.schabi.newpipe.extractor.StreamUrlIdHandler; | ||||
| import org.w3c.dom.Node; | ||||
| import org.w3c.dom.NodeList; | ||||
| import org.xml.sax.InputSource; | ||||
| import org.xml.sax.SAXException; | ||||
|  | ||||
| import java.net.URLEncoder; | ||||
| import java.io.ByteArrayInputStream; | ||||
| import java.io.IOException; | ||||
| import java.util.ArrayList; | ||||
|  | ||||
| import javax.xml.parsers.DocumentBuilder; | ||||
| import javax.xml.parsers.DocumentBuilderFactory; | ||||
| import javax.xml.parsers.ParserConfigurationException; | ||||
|  | ||||
| /** | ||||
|  * Created by Christian Schabesberger on 09.08.15. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org> | ||||
|  * YoutubeSearchEngine.java is part of NewPipe. | ||||
|  * | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * NewPipe is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| public class YoutubeSearchEngine extends SearchEngine { | ||||
|  | ||||
|     private static final String TAG = YoutubeSearchEngine.class.toString(); | ||||
|  | ||||
|     public YoutubeSearchEngine(StreamUrlIdHandler urlIdHandler, int serviceId) { | ||||
|         super(urlIdHandler, serviceId); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public StreamPreviewInfoCollector search(String query, int page, String languageCode, Downloader downloader) | ||||
|             throws IOException, ExtractionException { | ||||
|         StreamPreviewInfoCollector collector = getStreamPreviewInfoCollector(); | ||||
|  | ||||
|         /* Cant use Uri.Bilder since it's android code. | ||||
|         // Android code is baned from the extractor side. | ||||
|         Uri.Builder builder = new Uri.Builder(); | ||||
|         builder.scheme("https") | ||||
|                 .authority("www.youtube.com") | ||||
|                 .appendPath("results") | ||||
|                 .appendQueryParameter("search_query", query) | ||||
|                 .appendQueryParameter("page", Integer.toString(page)) | ||||
|                 .appendQueryParameter("filters", "video"); | ||||
|                 */ | ||||
|  | ||||
|         String url = "https://www.youtube.com/results" | ||||
|                 + "?search_query=" + URLEncoder.encode(query, "UTF-8") | ||||
|                 + "&page=" + Integer.toString(page) | ||||
|                 + "&filters=" + "video"; | ||||
|  | ||||
|         String site; | ||||
|         //String url = builder.build().toString(); | ||||
|         //if we've been passed a valid language code, append it to the URL | ||||
|         if(!languageCode.isEmpty()) { | ||||
|             //assert Pattern.matches("[a-z]{2}(-([A-Z]{2}|[0-9]{1,3}))?", languageCode); | ||||
|             site  = downloader.download(url, languageCode); | ||||
|         } | ||||
|         else { | ||||
|             site = downloader.download(url); | ||||
|         } | ||||
|  | ||||
|  | ||||
|         Document doc = Jsoup.parse(site, url); | ||||
|         Element list = doc.select("ol[class=\"item-section\"]").first(); | ||||
|  | ||||
|         for (Element item : list.children()) { | ||||
|             /* First we need to determine which kind of item we are working with. | ||||
|                Youtube depicts five different kinds of items on its search result page. These are | ||||
|                regular videos, playlists, channels, two types of video suggestions, and a "no video | ||||
|                found" item. Since we only want videos, we need to filter out all the others. | ||||
|                An example for this can be seen here: | ||||
|                https://www.youtube.com/results?search_query=asdf&page=1 | ||||
|  | ||||
|                We already applied a filter to the url, so we don't need to care about channels and | ||||
|                playlists now. | ||||
|             */ | ||||
|  | ||||
|             Element el; | ||||
|  | ||||
|             // both types of spell correction item | ||||
|             if (!((el = item.select("div[class*=\"spell-correction\"]").first()) == null)) { | ||||
|                 collector.setSuggestion(el.select("a").first().text()); | ||||
|                 if(list.children().size() == 1) { | ||||
|                     throw new NothingFoundException("Did you mean: " + el.select("a").first().text()); | ||||
|                 } | ||||
|                 // search message item | ||||
|             } else if (!((el = item.select("div[class*=\"search-message\"]").first()) == null)) { | ||||
|                 //result.errorMessage = el.text(); | ||||
|                 throw new NothingFoundException(el.text()); | ||||
|  | ||||
|                 // video item type | ||||
|             } else if (!((el = item.select("div[class*=\"yt-lockup-video\"").first()) == null)) { | ||||
|                 collector.commit(extractPreviewInfo(el)); | ||||
|             } else { | ||||
|                 //noinspection ConstantConditions | ||||
|                 collector.addError(new Exception("unexpected element found:\"" + el + "\"")); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return collector; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public ArrayList<String> suggestionList(String query, String contentCountry, Downloader dl) | ||||
|             throws IOException, ParsingException { | ||||
|  | ||||
|         ArrayList<String> suggestions = new ArrayList<>(); | ||||
|  | ||||
|         /* Cant use Uri.Bilder since it's android code. | ||||
|         // Android code is baned from the extractor side. | ||||
|         Uri.Builder builder = new Uri.Builder(); | ||||
|         builder.scheme("https") | ||||
|                 .authority("suggestqueries.google.com") | ||||
|                 .appendPath("complete") | ||||
|                 .appendPath("search") | ||||
|                 .appendQueryParameter("client", "") | ||||
|                 .appendQueryParameter("output", "toolbar") | ||||
|                 .appendQueryParameter("ds", "yt") | ||||
|                 .appendQueryParameter("hl",contentCountry) | ||||
|                 .appendQueryParameter("q", query); | ||||
|                 */ | ||||
|         String url = "https://suggestqueries.google.com/complete/search" | ||||
|                 + "?client=" + "" | ||||
|                 + "&output=" + "toolbar" | ||||
|                 + "&ds=" + "yt" | ||||
|                 + "&hl=" + URLEncoder.encode(contentCountry, "UTF-8") | ||||
|                 + "&q=" + URLEncoder.encode(query, "UTF-8"); | ||||
|  | ||||
|  | ||||
|         String response = dl.download(url); | ||||
|  | ||||
|         //TODO: Parse xml data using Jsoup not done | ||||
|         DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); | ||||
|         DocumentBuilder dBuilder; | ||||
|         org.w3c.dom.Document doc = null; | ||||
|  | ||||
|         try { | ||||
|             dBuilder = dbFactory.newDocumentBuilder(); | ||||
|             doc = dBuilder.parse(new InputSource( | ||||
|                     new ByteArrayInputStream(response.getBytes("utf-8")))); | ||||
|             doc.getDocumentElement().normalize(); | ||||
|         } catch (ParserConfigurationException | SAXException | IOException e) { | ||||
|             throw new ParsingException("Could not parse document."); | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             NodeList nList = doc.getElementsByTagName("CompleteSuggestion"); | ||||
|             for (int temp = 0; temp < nList.getLength(); temp++) { | ||||
|  | ||||
|                 NodeList nList1 = doc.getElementsByTagName("suggestion"); | ||||
|                 Node nNode1 = nList1.item(temp); | ||||
|                 if (nNode1.getNodeType() == Node.ELEMENT_NODE) { | ||||
|                     org.w3c.dom.Element eElement = (org.w3c.dom.Element) nNode1; | ||||
|                     suggestions.add(eElement.getAttribute("data")); | ||||
|                 } | ||||
|             } | ||||
|             return suggestions; | ||||
|         } catch(Exception e) { | ||||
|             throw new ParsingException("Could not get suggestions form document.", e); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private StreamPreviewInfoExtractor extractPreviewInfo(final Element item) { | ||||
|         return new YoutubeStreamPreviewInfoExtractor(item); | ||||
|     } | ||||
| } | ||||
| @@ -1,11 +1,11 @@ | ||||
| package org.schabi.newpipe.crawler.services.youtube; | ||||
| package org.schabi.newpipe.extractor.services.youtube; | ||||
| 
 | ||||
| import org.schabi.newpipe.crawler.CrawlingException; | ||||
| import org.schabi.newpipe.crawler.Downloader; | ||||
| import org.schabi.newpipe.crawler.StreamExtractor; | ||||
| import org.schabi.newpipe.crawler.StreamingService; | ||||
| import org.schabi.newpipe.crawler.VideoUrlIdHandler; | ||||
| import org.schabi.newpipe.crawler.SearchEngine; | ||||
| import org.schabi.newpipe.extractor.ExtractionException; | ||||
| import org.schabi.newpipe.extractor.Downloader; | ||||
| import org.schabi.newpipe.extractor.StreamExtractor; | ||||
| import org.schabi.newpipe.extractor.StreamingService; | ||||
| import org.schabi.newpipe.extractor.StreamUrlIdHandler; | ||||
| import org.schabi.newpipe.extractor.SearchEngine; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| 
 | ||||
| @@ -30,7 +30,12 @@ import java.io.IOException; | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
| 
 | ||||
| public class YoutubeService implements StreamingService { | ||||
| public class YoutubeService extends StreamingService { | ||||
| 
 | ||||
|     public YoutubeService(int id) { | ||||
|         super(id); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public ServiceInfo getServiceInfo() { | ||||
|         ServiceInfo serviceInfo = new ServiceInfo(); | ||||
| @@ -39,22 +44,22 @@ public class YoutubeService implements StreamingService { | ||||
|     } | ||||
|     @Override | ||||
|     public StreamExtractor getExtractorInstance(String url, Downloader downloader) | ||||
|             throws CrawlingException, IOException { | ||||
|         VideoUrlIdHandler urlIdHandler = new YoutubeVideoUrlIdHandler(); | ||||
|             throws ExtractionException, IOException { | ||||
|         StreamUrlIdHandler urlIdHandler = new YoutubeStreamUrlIdHandler(); | ||||
|         if(urlIdHandler.acceptUrl(url)) { | ||||
|             return new YoutubeStreamExtractor(url, downloader) ; | ||||
|             return new YoutubeStreamExtractor(url, downloader, getServiceId()); | ||||
|         } | ||||
|         else { | ||||
|             throw new IllegalArgumentException("supplied String is not a valid Youtube URL"); | ||||
|         } | ||||
|     } | ||||
|     @Override | ||||
|     public SearchEngine getSearchEngineInstance() { | ||||
|         return new YoutubeSearchEngine(); | ||||
|     public SearchEngine getSearchEngineInstance(Downloader downloader) { | ||||
|         return new YoutubeSearchEngine(getUrlIdHandlerInstance(), getServiceId()); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public VideoUrlIdHandler getUrlIdHandler() { | ||||
|         return new YoutubeVideoUrlIdHandler(); | ||||
|     public StreamUrlIdHandler getUrlIdHandlerInstance() { | ||||
|         return new YoutubeStreamUrlIdHandler(); | ||||
|     } | ||||
| } | ||||
| @@ -1,7 +1,4 @@ | ||||
| package org.schabi.newpipe.crawler.services.youtube; | ||||
| 
 | ||||
| import android.provider.MediaStore; | ||||
| import android.util.Log; | ||||
| package org.schabi.newpipe.extractor.services.youtube; | ||||
| 
 | ||||
| import org.json.JSONException; | ||||
| import org.json.JSONObject; | ||||
| @@ -11,22 +8,24 @@ import org.jsoup.nodes.Element; | ||||
| import org.mozilla.javascript.Context; | ||||
| import org.mozilla.javascript.Function; | ||||
| import org.mozilla.javascript.ScriptableObject; | ||||
| import org.schabi.newpipe.crawler.CrawlingException; | ||||
| import org.schabi.newpipe.crawler.Downloader; | ||||
| import org.schabi.newpipe.crawler.Parser; | ||||
| import org.schabi.newpipe.crawler.ParsingException; | ||||
| import org.schabi.newpipe.crawler.VideoUrlIdHandler; | ||||
| import org.schabi.newpipe.crawler.StreamExtractor; | ||||
| import org.schabi.newpipe.crawler.MediaFormat; | ||||
| import org.schabi.newpipe.crawler.VideoInfo; | ||||
| import org.schabi.newpipe.crawler.VideoPreviewInfo; | ||||
| import org.schabi.newpipe.extractor.AudioStream; | ||||
| import org.schabi.newpipe.extractor.ExtractionException; | ||||
| import org.schabi.newpipe.extractor.Downloader; | ||||
| import org.schabi.newpipe.extractor.Parser; | ||||
| import org.schabi.newpipe.extractor.ParsingException; | ||||
| import org.schabi.newpipe.extractor.StreamInfo; | ||||
| import org.schabi.newpipe.extractor.StreamPreviewInfo; | ||||
| import org.schabi.newpipe.extractor.StreamUrlIdHandler; | ||||
| import org.schabi.newpipe.extractor.StreamExtractor; | ||||
| import org.schabi.newpipe.extractor.MediaFormat; | ||||
| import org.schabi.newpipe.extractor.VideoStream; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| import java.net.URLDecoder; | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.Vector; | ||||
| import java.util.regex.Matcher; | ||||
| import java.util.regex.Pattern; | ||||
| 
 | ||||
| /** | ||||
|  * Created by Christian Schabesberger on 06.08.15. | ||||
| @@ -48,7 +47,41 @@ import java.util.Vector; | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
| 
 | ||||
| public class YoutubeStreamExtractor implements StreamExtractor { | ||||
| public class YoutubeStreamExtractor extends StreamExtractor { | ||||
| 
 | ||||
|     // exceptions | ||||
| 
 | ||||
|     public class DecryptException extends ParsingException { | ||||
|         DecryptException(String message, Throwable cause) { | ||||
|             super(message, cause); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // special content not available exceptions | ||||
| 
 | ||||
|     public class GemaException extends ContentNotAvailableException { | ||||
|         GemaException(String message) { | ||||
|             super(message); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public class LiveStreamException extends ContentNotAvailableException { | ||||
|         LiveStreamException(String message) { | ||||
|             super(message); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // ---------------- | ||||
| 
 | ||||
|     // Sometimes if the html page of youtube is already downloaded, youtube web page will internally | ||||
|     // download the /get_video_info page. Since a certain date dashmpd url is only available over | ||||
|     // this /get_video_info page, so we always need to download this one to. | ||||
|     // %%video_id%% will be replaced by the actual video id | ||||
|     // $$el_type$$ will be replaced by the actual el_type (se the declarations below) | ||||
|     private static final String GET_VIDEO_INFO_URL = | ||||
|             "https://www.youtube.com/get_video_info?video_id=%%video_id%%$$el_type$$&ps=default&eurl=&gl=US&hl=en"; | ||||
|     // eltype is nececeary for the url aboth | ||||
|     private static final String EL_INFO = "el=info"; | ||||
| 
 | ||||
|     public enum ItagType { | ||||
|         AUDIO, | ||||
| @@ -131,38 +164,10 @@ public class YoutubeStreamExtractor implements StreamExtractor { | ||||
|         throw new ParsingException("itag=" + Integer.toString(itag) + " not supported"); | ||||
|     } | ||||
| 
 | ||||
|     // Sometimes if the html page of youtube is already downloaded, youtube web page will internally | ||||
|     // download the /get_video_info page. Since a certain date dashmpd url is only available over | ||||
|     // this /get_video_info page, so we always need to download this one to. | ||||
|     // %%video_id%% will be replaced by the actual video id | ||||
|     // $$el_type$$ will be replaced by the actual el_type (se the declarations below) | ||||
|     private static final String GET_VIDEO_INFO_URL = | ||||
|             "https://www.youtube.com/get_video_info?video_id=%%video_id%%$$el_type$$&ps=default&eurl=&gl=US&hl=en"; | ||||
|     // eltype is nececeary for the url aboth | ||||
|     private static final String EL_INFO = "el=info"; | ||||
| 
 | ||||
|     public class DecryptException extends ParsingException { | ||||
|         DecryptException(Throwable cause) { | ||||
|             super(cause); | ||||
|         } | ||||
|         DecryptException(String message, Throwable cause) { | ||||
|             super(message, cause); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // special content not available exceptions | ||||
| 
 | ||||
|     public class GemaException extends ContentNotAvailableException { | ||||
|         GemaException(String message) { | ||||
|             super(message); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // ---------------- | ||||
| 
 | ||||
|     private static final String TAG = YoutubeStreamExtractor.class.toString(); | ||||
|     private final Document doc; | ||||
|     private JSONObject playerArgs; | ||||
|     private boolean isAgeRestricted; | ||||
|     private Map<String, String> videoInfoPage; | ||||
| 
 | ||||
|     // static values | ||||
| @@ -171,80 +176,140 @@ public class YoutubeStreamExtractor implements StreamExtractor { | ||||
|     // cached values | ||||
|     private static volatile String decryptionCode = ""; | ||||
| 
 | ||||
|     VideoUrlIdHandler urlidhandler = new YoutubeVideoUrlIdHandler(); | ||||
|     StreamUrlIdHandler urlidhandler = new YoutubeStreamUrlIdHandler(); | ||||
|     String pageUrl = ""; | ||||
| 
 | ||||
|     private Downloader downloader; | ||||
| 
 | ||||
|     public YoutubeStreamExtractor(String pageUrl, Downloader dl) throws CrawlingException, IOException { | ||||
|     public YoutubeStreamExtractor(String pageUrl, Downloader dl, int serviceId) | ||||
|             throws ExtractionException, IOException { | ||||
|         super(pageUrl, dl, serviceId); | ||||
|         //most common videoInfo fields are now set in our superclass, for all services | ||||
|         downloader = dl; | ||||
|         this.pageUrl = pageUrl; | ||||
|         String pageContent = downloader.download(urlidhandler.cleanUrl(pageUrl)); | ||||
|         doc = Jsoup.parse(pageContent, pageUrl); | ||||
|         String ytPlayerConfigRaw; | ||||
|         JSONObject ytPlayerConfig; | ||||
|         String playerUrl; | ||||
| 
 | ||||
|         //attempt to load the youtube js player JSON arguments | ||||
|         // Check if the video is age restricted | ||||
|         if (pageContent.contains("<meta property=\"og:restrictions:age")) { | ||||
|             String videoInfoUrl = GET_VIDEO_INFO_URL.replace("%%video_id%%", | ||||
|                     urlidhandler.getVideoId(pageUrl)).replace("$$el_type$$", "&" + EL_INFO); | ||||
|             String videoInfoPageString = downloader.download(videoInfoUrl); | ||||
|             videoInfoPage = Parser.compatParseMap(videoInfoPageString); | ||||
|             playerUrl = getPlayerUrlFromRestrictedVideo(pageUrl); | ||||
|             isAgeRestricted = true; | ||||
|         } else { | ||||
|             ytPlayerConfig = getPlayerConfig(pageContent); | ||||
|             playerArgs = getPlayerArgs(ytPlayerConfig); | ||||
|             playerUrl = getPlayerUrl(ytPlayerConfig); | ||||
|             isAgeRestricted = false; | ||||
|         } | ||||
| 
 | ||||
|         if(decryptionCode.isEmpty()) { | ||||
|             decryptionCode = loadDecryptionCode(playerUrl); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private JSONObject getPlayerConfig(String pageContent) throws ParsingException { | ||||
|         try { | ||||
|             ytPlayerConfigRaw = | ||||
|             String ytPlayerConfigRaw = | ||||
|                     Parser.matchGroup1("ytplayer.config\\s*=\\s*(\\{.*?\\});", pageContent); | ||||
|             ytPlayerConfig = new JSONObject(ytPlayerConfigRaw); | ||||
|             playerArgs = ytPlayerConfig.getJSONObject("args"); | ||||
|             return new JSONObject(ytPlayerConfigRaw); | ||||
|         } catch (Parser.RegexException e) { | ||||
|             String errorReason = findErrorReason(doc); | ||||
|             switch(errorReason) { | ||||
|                 case "GEMA": | ||||
|                     throw new GemaException(errorReason); | ||||
|                 case "": | ||||
|                     throw new ParsingException("player config empty", e); | ||||
|                     throw new ContentNotAvailableException("Content not available: player config empty", e); | ||||
|                 default: | ||||
|                     throw new ContentNotAvailableException("Content not available", e); | ||||
|             } | ||||
|         } catch (JSONException e) { | ||||
|             throw new ParsingException("Could not parse yt player config"); | ||||
|             throw new ParsingException("Could not parse yt player config", e); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private JSONObject getPlayerArgs(JSONObject playerConfig) throws ParsingException { | ||||
|         JSONObject playerArgs; | ||||
| 
 | ||||
|         // get videoInfo page | ||||
|         //attempt to load the youtube js player JSON arguments | ||||
|         boolean isLiveStream = false; //used to determine if this is a livestream or not | ||||
|         try { | ||||
|             //Parser.unescapeEntities(url_data_str, true).split("&") | ||||
|             String getVideoInfoUrl = GET_VIDEO_INFO_URL.replace("%%video_id%%", | ||||
|                     urlidhandler.getVideoId(pageUrl)).replace("$$el_type$$", "&" + EL_INFO); | ||||
|             videoInfoPage = Parser.compatParseMap(downloader.download(getVideoInfoUrl)); | ||||
|         } catch(Exception e) { | ||||
|             throw new ParsingException("Could not load video info page.", e); | ||||
|             playerArgs = playerConfig.getJSONObject("args"); | ||||
| 
 | ||||
|             // check if we have a live stream. We need to filter it, since its not yet supported. | ||||
|             if((playerArgs.has("ps") && playerArgs.get("ps").toString().equals("live")) | ||||
|                     || (playerArgs.get("url_encoded_fmt_stream_map").toString().isEmpty())) { | ||||
|                 isLiveStream = true; | ||||
|             } | ||||
|         }  catch (JSONException e) { | ||||
|             throw new ParsingException("Could not parse yt player config", e); | ||||
|         } | ||||
|         if (isLiveStream) { | ||||
|             throw new LiveStreamException("This is a Life stream. Can't use those right now."); | ||||
|         } | ||||
| 
 | ||||
|         //---------------------------------- | ||||
|         // load and parse description code, if it isn't already initialised | ||||
|         //---------------------------------- | ||||
|         if (decryptionCode.isEmpty()) { | ||||
|         return playerArgs; | ||||
|     } | ||||
| 
 | ||||
|     private String getPlayerUrl(JSONObject playerConfig) throws ParsingException { | ||||
|         try { | ||||
|             // The Youtube service needs to be initialized by downloading the | ||||
|             // js-Youtube-player. This is done in order to get the algorithm | ||||
|             // for decrypting cryptic signatures inside certain stream urls. | ||||
|                 JSONObject ytAssets = ytPlayerConfig.getJSONObject("assets"); | ||||
|                 String playerUrl = ytAssets.getString("js"); | ||||
|             String playerUrl = ""; | ||||
| 
 | ||||
|             JSONObject ytAssets = playerConfig.getJSONObject("assets"); | ||||
|             playerUrl = ytAssets.getString("js"); | ||||
| 
 | ||||
|             if (playerUrl.startsWith("//")) { | ||||
|                 playerUrl = "https:" + playerUrl; | ||||
|             } | ||||
|                 decryptionCode = loadDecryptionCode(playerUrl); | ||||
|             return playerUrl; | ||||
|         } catch (JSONException e) { | ||||
|             throw new ParsingException( | ||||
|                     "Could not load decryption code for the Youtube service.", e); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private String getPlayerUrlFromRestrictedVideo(String pageUrl) throws ParsingException { | ||||
|         try { | ||||
|             String playerUrl = ""; | ||||
|             String videoId = urlidhandler.getVideoId(pageUrl); | ||||
|             String embedUrl = "https://www.youtube.com/embed/" + videoId; | ||||
|             String embedPageContent = downloader.download(embedUrl); | ||||
|             //todo: find out if this can be reapaced by Parser.matchGroup1() | ||||
|             Pattern assetsPattern = Pattern.compile("\"assets\":.+?\"js\":\\s*(\"[^\"]+\")"); | ||||
|             Matcher patternMatcher = assetsPattern.matcher(embedPageContent); | ||||
|             while (patternMatcher.find()) { | ||||
|                 playerUrl = patternMatcher.group(1); | ||||
|             } | ||||
|             playerUrl = playerUrl.replace("\\", "").replace("\"", ""); | ||||
| 
 | ||||
|             if (playerUrl.startsWith("//")) { | ||||
|                 playerUrl = "https:" + playerUrl; | ||||
|             } | ||||
|             return playerUrl; | ||||
|         } catch (IOException e) { | ||||
|             throw new ParsingException( | ||||
|                     "Could load decryption code form restricted video for the Youtube service.", e); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public String getTitle() throws ParsingException { | ||||
|         try {//json player args method | ||||
|         try { | ||||
|             if (playerArgs == null) { | ||||
|                 return videoInfoPage.get("title"); | ||||
|             } | ||||
|             //json player args method | ||||
|             return playerArgs.getString("title"); | ||||
|         } catch(JSONException je) {//html <meta> method | ||||
|             je.printStackTrace(); | ||||
|             Log.w(TAG, "failed to load title from JSON args; trying to extract it from HTML"); | ||||
|             System.err.println("failed to load title from JSON args; trying to extract it from HTML"); | ||||
|             try { // fall through to fall-back | ||||
|                 return doc.select("meta[name=title]").attr("content"); | ||||
|             } catch (Exception e) { | ||||
| @@ -264,11 +329,15 @@ public class YoutubeStreamExtractor implements StreamExtractor { | ||||
| 
 | ||||
|     @Override | ||||
|     public String getUploader() throws ParsingException { | ||||
|         try {//json player args method | ||||
|         try { | ||||
|             if (playerArgs == null) { | ||||
|                 return videoInfoPage.get("author"); | ||||
|             } | ||||
|             //json player args method | ||||
|             return playerArgs.getString("author"); | ||||
|         } catch(JSONException je) { | ||||
|             je.printStackTrace(); | ||||
|             Log.w(TAG, | ||||
|             System.err.println( | ||||
|                     "failed to load uploader name from JSON args; trying to extract it from HTML"); | ||||
|         } try {//fall through to fallback HTML method | ||||
|             return doc.select("div.yt-user-info").first().text(); | ||||
| @@ -280,6 +349,9 @@ public class YoutubeStreamExtractor implements StreamExtractor { | ||||
|     @Override | ||||
|     public int getLength() throws ParsingException { | ||||
|         try { | ||||
|             if (playerArgs == null) { | ||||
|                 return Integer.valueOf(videoInfoPage.get("length_seconds")); | ||||
|             } | ||||
|             return playerArgs.getInt("length_seconds"); | ||||
|         } catch (JSONException e) {//todo: find fallback method | ||||
|             throw new ParsingException("failed to load video duration from JSON args", e); | ||||
| @@ -287,12 +359,12 @@ public class YoutubeStreamExtractor implements StreamExtractor { | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public long getViews() throws ParsingException { | ||||
|     public long getViewCount() throws ParsingException { | ||||
|         try { | ||||
|             String viewCountString = doc.select("meta[itemprop=interactionCount]").attr("content"); | ||||
|             return Long.parseLong(viewCountString); | ||||
|         } catch (Exception e) {//todo: find fallback method | ||||
|             throw new ParsingException("failed to number of views", e); | ||||
|             throw new ParsingException("failed to get number of views", e); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @@ -313,13 +385,16 @@ public class YoutubeStreamExtractor implements StreamExtractor { | ||||
|         try { | ||||
|             return doc.select("link[itemprop=\"thumbnailUrl\"]").first().attr("abs:href"); | ||||
|         } catch(Exception e) { | ||||
|             Log.w(TAG, "Could not find high res Thumbnail. Using low res instead"); | ||||
|             System.err.println("Could not find high res Thumbnail. Using low res instead"); | ||||
|         } | ||||
|         try { //fall through to fallback | ||||
|             return playerArgs.getString("thumbnail_url"); | ||||
|         } catch (JSONException je) { | ||||
|             throw new ParsingException( | ||||
|                     "failed to extract thumbnail URL from JSON args; trying to extract it from HTML", je); | ||||
|         } catch (NullPointerException ne) { | ||||
|             // Get from the video info page instead | ||||
|             return videoInfoPage.get("thumbnail_url"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @@ -337,24 +412,6 @@ public class YoutubeStreamExtractor implements StreamExtractor { | ||||
|     @Override | ||||
|     public String getDashMpdUrl() throws ParsingException { | ||||
|         /* | ||||
|         try { | ||||
|             String dashManifest = playerArgs.getString("dashmpd"); | ||||
|             if(!dashManifest.contains("/signature/")) { | ||||
|                 String encryptedSig = Parser.matchGroup1("/s/([a-fA-F0-9\\.]+)", dashManifest); | ||||
|                 String decryptedSig; | ||||
| 
 | ||||
|                 decryptedSig = decryptSignature(encryptedSig, decryptionCode); | ||||
|                 dashManifest = dashManifest.replace("/s/" + encryptedSig, "/signature/" + decryptedSig); | ||||
|             } | ||||
| 
 | ||||
|             return dashManifest; | ||||
|         } catch(JSONException je) { | ||||
|             throw new ParsingException( | ||||
|                     "Could not find \"dashmpd\" upon the player args (maybe no dash manifest available).", je); | ||||
|         } catch (Exception e) { | ||||
|             throw new ParsingException(e); | ||||
|         } | ||||
|         */ | ||||
|         try { | ||||
|             String dashManifestUrl = videoInfoPage.get("dashmpd"); | ||||
|             if(!dashManifestUrl.contains("/signature/")) { | ||||
| @@ -369,15 +426,23 @@ public class YoutubeStreamExtractor implements StreamExtractor { | ||||
|             throw new ParsingException( | ||||
|                     "Could not get \"dashmpd\" maybe VideoInfoPage is broken.", e); | ||||
|         } | ||||
|         */ | ||||
|         return ""; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     @Override | ||||
|     public List<VideoInfo.AudioStream> getAudioStreams() throws ParsingException { | ||||
|         Vector<VideoInfo.AudioStream> audioStreams = new Vector<>(); | ||||
|     public List<AudioStream> getAudioStreams() throws ParsingException { | ||||
|         Vector<AudioStream> audioStreams = new Vector<>(); | ||||
|         try{ | ||||
|             String encoded_url_map = playerArgs.getString("adaptive_fmts"); | ||||
|             for(String url_data_str : encoded_url_map.split(",")) { | ||||
|             String encodedUrlMap; | ||||
|             // playerArgs could be null if the video is age restricted | ||||
|             if (playerArgs == null) { | ||||
|                 encodedUrlMap = videoInfoPage.get("adaptive_fmts"); | ||||
|             } else { | ||||
|                 encodedUrlMap = playerArgs.getString("adaptive_fmts"); | ||||
|             } | ||||
|             for(String url_data_str : encodedUrlMap.split(",")) { | ||||
|                 // This loop iterates through multiple streams, therefor tags | ||||
|                 // is related to one and the same stream at a time. | ||||
|                 Map<String, String> tags = Parser.compatParseMap( | ||||
| @@ -395,7 +460,7 @@ public class YoutubeStreamExtractor implements StreamExtractor { | ||||
|                                     + decryptSignature(tags.get("s"), decryptionCode); | ||||
|                         } | ||||
| 
 | ||||
|                         audioStreams.add(new VideoInfo.AudioStream(streamUrl, | ||||
|                         audioStreams.add(new AudioStream(streamUrl, | ||||
|                                 itagItem.mediaFormatId, | ||||
|                                 itagItem.bandWidth, | ||||
|                                 itagItem.samplingRate)); | ||||
| @@ -409,12 +474,18 @@ public class YoutubeStreamExtractor implements StreamExtractor { | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public List<VideoInfo.VideoStream> getVideoStreams() throws ParsingException { | ||||
|         Vector<VideoInfo.VideoStream> videoStreams = new Vector<>(); | ||||
|     public List<VideoStream> getVideoStreams() throws ParsingException { | ||||
|         Vector<VideoStream> videoStreams = new Vector<>(); | ||||
| 
 | ||||
|         try{ | ||||
|             String encoded_url_map = playerArgs.getString("url_encoded_fmt_stream_map"); | ||||
|             for(String url_data_str : encoded_url_map.split(",")) { | ||||
|             String encodedUrlMap; | ||||
|             // playerArgs could be null if the video is age restricted | ||||
|             if (playerArgs == null) { | ||||
|                 encodedUrlMap = videoInfoPage.get("url_encoded_fmt_stream_map"); | ||||
|             } else { | ||||
|                 encodedUrlMap = playerArgs.getString("url_encoded_fmt_stream_map"); | ||||
|             } | ||||
|             for(String url_data_str : encodedUrlMap.split(",")) { | ||||
|                 try { | ||||
|                     // This loop iterates through multiple streams, therefor tags | ||||
|                     // is related to one and the same stream at a time. | ||||
| @@ -432,14 +503,15 @@ public class YoutubeStreamExtractor implements StreamExtractor { | ||||
|                                 streamUrl = streamUrl + "&signature=" | ||||
|                                         + decryptSignature(tags.get("s"), decryptionCode); | ||||
|                             } | ||||
|                             videoStreams.add(new VideoInfo.VideoStream( | ||||
|                             videoStreams.add(new VideoStream( | ||||
|                                     streamUrl, | ||||
|                                     itagItem.mediaFormatId, | ||||
|                                     itagItem.resolutionString)); | ||||
|                         } | ||||
|                     } | ||||
|                 } catch (Exception e) { | ||||
|                     Log.w(TAG, "Could not get Video stream."); | ||||
|                     //todo: dont log throw an error | ||||
|                     System.err.println("Could not get Video stream."); | ||||
|                     e.printStackTrace(); | ||||
|                 } | ||||
|             } | ||||
| @@ -455,7 +527,7 @@ public class YoutubeStreamExtractor implements StreamExtractor { | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public List<VideoInfo.VideoStream> getVideoOnlyStreams() throws ParsingException { | ||||
|     public List<VideoStream> getVideoOnlyStreams() throws ParsingException { | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
| @@ -492,13 +564,13 @@ public class YoutubeStreamExtractor implements StreamExtractor { | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 int seconds = (secondsString.isEmpty() ? 0 : Integer.parseInt(secondsString)); | ||||
|                 int minutes = (minutesString.isEmpty() ? 0 : Integer.parseInt(minutesString)); | ||||
|                 int hours = (hoursString.isEmpty() ? 0 : Integer.parseInt(hoursString)); | ||||
|                 int seconds = secondsString.isEmpty() ? 0 : Integer.parseInt(secondsString); | ||||
|                 int minutes = minutesString.isEmpty() ? 0 : Integer.parseInt(minutesString); | ||||
|                 int hours = hoursString.isEmpty() ? 0 : Integer.parseInt(hoursString); | ||||
| 
 | ||||
|                 int ret = seconds + (60 * minutes) + (3600 * hours);//don't trust BODMAS! | ||||
|                 //don't trust BODMAS! | ||||
|                 return seconds + (60 * minutes) + (3600 * hours); | ||||
|                 //Log.d(TAG, "derived timestamp value:"+ret); | ||||
|                 return ret; | ||||
|                 //the ordering varies internationally | ||||
|             } catch (ParsingException e) { | ||||
|                 throw new ParsingException("Could not get timestamp.", e); | ||||
| @@ -510,15 +582,24 @@ public class YoutubeStreamExtractor implements StreamExtractor { | ||||
| 
 | ||||
|     @Override | ||||
|     public int getAgeLimit() throws ParsingException { | ||||
|         // Not yet implemented. | ||||
|         // Also you need to be logged in to see age restricted videos on youtube, | ||||
|         // therefore NP is not able to receive such videos. | ||||
|         if (!isAgeRestricted) { | ||||
|             return 0; | ||||
|         } | ||||
|         try { | ||||
|             return Integer.valueOf(doc.head() | ||||
|                     .getElementsByAttributeValue("property", "og:restrictions:age") | ||||
|                     .attr("content").replace("+", "")); | ||||
|         } catch (Exception e) { | ||||
|             throw new ParsingException("Could not get age restriction"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public String getAverageRating() throws ParsingException { | ||||
|         try { | ||||
|             if (playerArgs == null) { | ||||
|                 return videoInfoPage.get("avg_rating"); | ||||
|             } | ||||
|             return playerArgs.getString("avg_rating"); | ||||
|         } catch (JSONException e) { | ||||
|             throw new ParsingException("Could not get Average rating", e); | ||||
| @@ -529,8 +610,14 @@ public class YoutubeStreamExtractor implements StreamExtractor { | ||||
|     public int getLikeCount() throws ParsingException { | ||||
|         String likesString = ""; | ||||
|         try { | ||||
|             likesString = doc.select("button.like-button-renderer-like-button").first() | ||||
|                     .select("span.yt-uix-button-content").first().text(); | ||||
| 
 | ||||
|             Element button = doc.select("button.like-button-renderer-like-button").first(); | ||||
|             try { | ||||
|                 likesString = button.select("span.yt-uix-button-content").first().text(); | ||||
|             } catch (NullPointerException e) { | ||||
|                 //if this ckicks in our button has no content and thefore likes/dislikes are disabled | ||||
|                 return -1; | ||||
|             } | ||||
|             return Integer.parseInt(likesString.replaceAll("[^\\d]", "")); | ||||
|         } catch (NumberFormatException nfe) { | ||||
|             throw new ParsingException( | ||||
| @@ -544,8 +631,13 @@ public class YoutubeStreamExtractor implements StreamExtractor { | ||||
|     public int getDislikeCount() throws ParsingException { | ||||
|         String dislikesString = ""; | ||||
|         try { | ||||
|             dislikesString = doc.select("button.like-button-renderer-dislike-button").first() | ||||
|                     .select("span.yt-uix-button-content").first().text(); | ||||
|             Element button = doc.select("button.like-button-renderer-dislike-button").first(); | ||||
|             try { | ||||
|                 dislikesString = button.select("span.yt-uix-button-content").first().text(); | ||||
|             } catch (NullPointerException e) { | ||||
|                 //if this kicks in our button has no content and therefore likes/dislikes are disabled | ||||
|                 return -1; | ||||
|             } | ||||
|             return Integer.parseInt(dislikesString.replaceAll("[^\\d]", "")); | ||||
|         } catch(NumberFormatException nfe) { | ||||
|             throw new ParsingException( | ||||
| @@ -556,7 +648,7 @@ public class YoutubeStreamExtractor implements StreamExtractor { | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public VideoPreviewInfo getNextVideo() throws ParsingException { | ||||
|     public StreamPreviewInfo getNextVideo() throws ParsingException { | ||||
|         try { | ||||
|             return extractVideoPreviewInfo(doc.select("div[class=\"watch-sidebar-section\"]").first() | ||||
|                     .select("li").first()); | ||||
| @@ -566,9 +658,9 @@ public class YoutubeStreamExtractor implements StreamExtractor { | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public Vector<VideoPreviewInfo> getRelatedVideos() throws ParsingException { | ||||
|     public Vector<StreamPreviewInfo> getRelatedVideos() throws ParsingException { | ||||
|         try { | ||||
|             Vector<VideoPreviewInfo> relatedVideos = new Vector<>(); | ||||
|             Vector<StreamPreviewInfo> relatedVideos = new Vector<>(); | ||||
|             for (Element li : doc.select("ul[id=\"watch-related\"]").first().children()) { | ||||
|                 // first check if we have a playlist. If so leave them out | ||||
|                 if (li.select("a[class*=\"content-link\"]").first() != null) { | ||||
| @@ -582,8 +674,8 @@ public class YoutubeStreamExtractor implements StreamExtractor { | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public VideoUrlIdHandler getUrlIdConverter() { | ||||
|         return new YoutubeVideoUrlIdHandler(); | ||||
|     public StreamUrlIdHandler getUrlIdConverter() { | ||||
|         return new YoutubeStreamUrlIdHandler(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
| @@ -591,11 +683,17 @@ public class YoutubeStreamExtractor implements StreamExtractor { | ||||
|         return pageUrl; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public StreamInfo.StreamType getStreamType() throws ParsingException { | ||||
|         //todo: if implementing livestream support this value should be generated dynamically | ||||
|         return StreamInfo.StreamType.VIDEO_STREAM; | ||||
|     } | ||||
| 
 | ||||
|     /**Provides information about links to other videos on the video page, such as related videos. | ||||
|      * This is encapsulated in a VideoPreviewInfo object, | ||||
|      * which is a subset of the fields in a full VideoInfo.*/ | ||||
|     private VideoPreviewInfo extractVideoPreviewInfo(Element li) throws ParsingException { | ||||
|         VideoPreviewInfo info = new VideoPreviewInfo(); | ||||
|      * This is encapsulated in a StreamPreviewInfo object, | ||||
|      * which is a subset of the fields in a full StreamInfo.*/ | ||||
|     private StreamPreviewInfo extractVideoPreviewInfo(Element li) throws ParsingException { | ||||
|         StreamPreviewInfo info = new StreamPreviewInfo(); | ||||
| 
 | ||||
|         try { | ||||
|             info.webpage_url = li.select("a.content-link").first() | ||||
| @@ -617,12 +715,13 @@ public class YoutubeStreamExtractor implements StreamExtractor { | ||||
|             try { | ||||
|                 info.view_count = Long.parseLong(li.select("span.view-count") | ||||
|                         .first().text().replaceAll("[^\\d]", "")); | ||||
|             } catch (NullPointerException e) {//related videos sometimes have no view count | ||||
|             } catch (Exception e) {//related videos sometimes have no view count | ||||
|                 info.view_count = 0; | ||||
|             } | ||||
|             info.uploader = li.select("span.g-hovercard").first().text(); | ||||
| 
 | ||||
|             info.duration = li.select("span.video-time").first().text(); | ||||
|             info.duration = YoutubeParsingHelper.parseDurationString( | ||||
|                     li.select("span.video-time").first().text()); | ||||
| 
 | ||||
|             Element img = li.select("img").first(); | ||||
|             info.thumbnail_url = img.attr("abs:src"); | ||||
| @@ -636,7 +735,7 @@ public class YoutubeStreamExtractor implements StreamExtractor { | ||||
|                 info.thumbnail_url = "https:" + info.thumbnail_url; | ||||
|             } | ||||
|         } catch (Exception e) { | ||||
|             throw new ParsingException(e); | ||||
|             throw new ParsingException("Could not get video preview info", e); | ||||
|         } | ||||
|         return info; | ||||
|     } | ||||
| @@ -690,11 +789,11 @@ public class YoutubeStreamExtractor implements StreamExtractor { | ||||
|             Function decryptionFunc = (Function) scope.get("decrypt", scope); | ||||
|             result = decryptionFunc.call(context, scope, scope, new Object[]{encryptedSig}); | ||||
|         } catch (Exception e) { | ||||
|             throw new DecryptException(e); | ||||
|             throw new DecryptException("could not get decrypt signature", e); | ||||
|         } finally { | ||||
|             Context.exit(); | ||||
|         } | ||||
|         return (result == null ? "" : result.toString()); | ||||
|         return result == null ? "" : result.toString(); | ||||
|     } | ||||
| 
 | ||||
|     private String findErrorReason(Document doc) { | ||||
| @@ -0,0 +1,171 @@ | ||||
| package org.schabi.newpipe.extractor.services.youtube; | ||||
|  | ||||
| import org.jsoup.nodes.Element; | ||||
| import org.schabi.newpipe.extractor.AbstractVideoInfo; | ||||
| import org.schabi.newpipe.extractor.Parser; | ||||
| import org.schabi.newpipe.extractor.ParsingException; | ||||
| import org.schabi.newpipe.extractor.StreamPreviewInfoExtractor; | ||||
|  | ||||
| /** | ||||
|  * Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org> | ||||
|  * YoutubeStreamPreviewInfoExtractor.java is part of NewPipe. | ||||
|  * | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * NewPipe is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| public class YoutubeStreamPreviewInfoExtractor implements StreamPreviewInfoExtractor { | ||||
|  | ||||
|     private final Element item; | ||||
|  | ||||
|     public YoutubeStreamPreviewInfoExtractor(Element item) { | ||||
|         this.item = item; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String getWebPageUrl() throws ParsingException { | ||||
|         try { | ||||
|             Element el = item.select("div[class*=\"yt-lockup-video\"").first(); | ||||
|             Element dl = el.select("h3").first().select("a").first(); | ||||
|             return dl.attr("abs:href"); | ||||
|         } catch (Exception e) { | ||||
|             throw new ParsingException("Could not get web page url for the video", e); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String getTitle() throws ParsingException { | ||||
|         try { | ||||
|             Element el = item.select("div[class*=\"yt-lockup-video\"").first(); | ||||
|             Element dl = el.select("h3").first().select("a").first(); | ||||
|             return dl.text(); | ||||
|         } catch (Exception e) { | ||||
|             throw new ParsingException("Could not get title", e); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int getDuration() throws ParsingException { | ||||
|         try { | ||||
|             return YoutubeParsingHelper.parseDurationString( | ||||
|                     item.select("span[class=\"video-time\"]").first().text()); | ||||
|         } catch(Exception e) { | ||||
|             if(isLiveStream(item)) { | ||||
|                 // -1 for no duration | ||||
|                 return -1; | ||||
|             } else { | ||||
|                 throw new ParsingException("Could not get Duration: " + getTitle(), e); | ||||
|             } | ||||
|  | ||||
|  | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String getUploader() throws ParsingException { | ||||
|         try { | ||||
|             return item.select("div[class=\"yt-lockup-byline\"]").first() | ||||
|                     .select("a").first() | ||||
|                     .text(); | ||||
|         } catch (Exception e) { | ||||
|             throw new ParsingException("Could not get uploader", e); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String getUploadDate() throws ParsingException { | ||||
|         try { | ||||
|             return item.select("div[class=\"yt-lockup-meta\"]").first() | ||||
|                     .select("li").first() | ||||
|                     .text(); | ||||
|         } catch(Exception e) { | ||||
|             throw new ParsingException("Could not get uplaod date", e); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public long getViewCount() throws ParsingException { | ||||
|         String output; | ||||
|         String input; | ||||
|         try { | ||||
|             input = item.select("div[class=\"yt-lockup-meta\"]").first() | ||||
|                     .select("li").get(1) | ||||
|                     .text(); | ||||
|         } catch (IndexOutOfBoundsException e) { | ||||
|             if(isLiveStream(item)) { | ||||
|                 // -1 for no view count | ||||
|                 return -1; | ||||
|             } else { | ||||
|                 throw new ParsingException( | ||||
|                         "Could not parse yt-lockup-meta although available: " + getTitle(), e); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         output = Parser.matchGroup1("([0-9,\\. ]*)", input) | ||||
|                 .replace(" ", "") | ||||
|                 .replace(".", "") | ||||
|                 .replace(",", ""); | ||||
|  | ||||
|         try { | ||||
|             return Long.parseLong(output); | ||||
|         } catch (NumberFormatException e) { | ||||
|             // if this happens the video probably has no views | ||||
|             if(!input.isEmpty()) { | ||||
|                 return 0; | ||||
|             } else { | ||||
|                 throw new ParsingException("Could not handle input: " + input, e); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String getThumbnailUrl() throws ParsingException { | ||||
|         try { | ||||
|             String url; | ||||
|             Element te = item.select("div[class=\"yt-thumb video-thumb\"]").first() | ||||
|                     .select("img").first(); | ||||
|             url = te.attr("abs:src"); | ||||
|             // Sometimes youtube sends links to gif files which somehow seem to not exist | ||||
|             // anymore. Items with such gif also offer a secondary image source. So we are going | ||||
|             // to use that if we've caught such an item. | ||||
|             if (url.contains(".gif")) { | ||||
|                 url = te.attr("abs:data-thumb"); | ||||
|             } | ||||
|             return url; | ||||
|         } catch (Exception e) { | ||||
|             throw new ParsingException("Could not get thumbnail url", e); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public AbstractVideoInfo.StreamType getStreamType() { | ||||
|         if(isLiveStream(item)) { | ||||
|             return AbstractVideoInfo.StreamType.LIVE_STREAM; | ||||
|         } else { | ||||
|             return AbstractVideoInfo.StreamType.VIDEO_STREAM; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private boolean isLiveStream(Element item) { | ||||
|         Element bla = item.select("span[class*=\"yt-badge-live\"]").first(); | ||||
|  | ||||
|         if(bla == null) { | ||||
|             // sometimes livestreams dont have badges but sill are live streams | ||||
|             // if video time is not available we most likly have an offline livestream | ||||
|             if(item.select("span[class*=\"video-time\"]").first() == null) { | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
|         return bla != null; | ||||
|     } | ||||
| } | ||||
| @@ -1,14 +1,17 @@ | ||||
| package org.schabi.newpipe.crawler.services.youtube; | ||||
| package org.schabi.newpipe.extractor.services.youtube; | ||||
| 
 | ||||
| import org.schabi.newpipe.crawler.Parser; | ||||
| import org.schabi.newpipe.crawler.ParsingException; | ||||
| import org.schabi.newpipe.crawler.VideoUrlIdHandler; | ||||
| import org.schabi.newpipe.extractor.Parser; | ||||
| import org.schabi.newpipe.extractor.ParsingException; | ||||
| import org.schabi.newpipe.extractor.StreamUrlIdHandler; | ||||
| 
 | ||||
| import java.io.UnsupportedEncodingException; | ||||
| import java.net.URLDecoder; | ||||
| 
 | ||||
| /** | ||||
|  * Created by Christian Schabesberger on 02.02.16. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org> | ||||
|  * YoutubeVideoUrlIdHandler.java is part of NewPipe. | ||||
|  * YoutubeStreamUrlIdHandler.java is part of NewPipe. | ||||
|  * | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
| @@ -24,7 +27,7 @@ import org.schabi.newpipe.crawler.VideoUrlIdHandler; | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
| 
 | ||||
| public class YoutubeVideoUrlIdHandler implements VideoUrlIdHandler { | ||||
| public class YoutubeStreamUrlIdHandler implements StreamUrlIdHandler { | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     @Override | ||||
|     public String getVideoUrl(String videoId) { | ||||
| @@ -35,21 +38,33 @@ public class YoutubeVideoUrlIdHandler implements VideoUrlIdHandler { | ||||
|     @Override | ||||
|     public String getVideoId(String url) throws ParsingException { | ||||
|         String id; | ||||
|         String pat; | ||||
| 
 | ||||
|         if(url.contains("youtube")) { | ||||
|             pat = "youtube\\.com/watch\\?v=([\\-a-zA-Z0-9_]{11})"; | ||||
|             if(url.contains("attribution_link")) { | ||||
|                 try { | ||||
|                     String escapedQuery = Parser.matchGroup1("u=(.[^&|$]*)", url); | ||||
|                     String query = URLDecoder.decode(escapedQuery, "UTF-8"); | ||||
|                     id = Parser.matchGroup1("v=([\\-a-zA-Z0-9_]{11})", query); | ||||
|                 } catch(UnsupportedEncodingException uee) { | ||||
|                     throw new ParsingException("Could not parse attribution_link", uee); | ||||
|                 } | ||||
|             } else { | ||||
|                 id = Parser.matchGroup1("[?&]v=([\\-a-zA-Z0-9_]{11})", url); | ||||
|             } | ||||
|         } | ||||
|         else if(url.contains("youtu.be")) { | ||||
|             pat = "youtu\\.be/([a-zA-Z0-9_-]{11})"; | ||||
|             if(url.contains("v=")) { | ||||
|                 id = Parser.matchGroup1("v=([\\-a-zA-Z0-9_]{11})", url); | ||||
|             } else { | ||||
|                 id = Parser.matchGroup1("youtu\\.be/([a-zA-Z0-9_-]{11})", url); | ||||
|             } | ||||
|         } | ||||
|         else { | ||||
|             throw new ParsingException("Error no suitable url: " + url); | ||||
|         } | ||||
| 
 | ||||
|         id = Parser.matchGroup1(pat, url); | ||||
| 
 | ||||
|         if(!id.isEmpty()){ | ||||
|             //Log.i(TAG, "string \""+url+"\" matches!"); | ||||
|             return id; | ||||
|         } else { | ||||
|             throw new ParsingException("Error could not parse url: " + url); | ||||
| @@ -1,4 +1,4 @@ | ||||
| package org.schabi.newpipe; | ||||
| package org.schabi.newpipe.player; | ||||
| 
 | ||||
| import android.app.Notification; | ||||
| import android.app.NotificationManager; | ||||
| @@ -20,6 +20,12 @@ import android.util.Log; | ||||
| import android.widget.RemoteViews; | ||||
| import android.widget.Toast; | ||||
| 
 | ||||
| import org.schabi.newpipe.ActivityCommunicator; | ||||
| import org.schabi.newpipe.BuildConfig; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.VideoItemDetailActivity; | ||||
| import org.schabi.newpipe.VideoItemDetailFragment; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| 
 | ||||
| /** | ||||
| @@ -151,7 +157,7 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare | ||||
|                 e.printStackTrace(); | ||||
|             } | ||||
| 
 | ||||
|             WifiManager wifiMgr = ((WifiManager)getSystemService(Context.WIFI_SERVICE)); | ||||
|             WifiManager wifiMgr = (WifiManager)getSystemService(Context.WIFI_SERVICE); | ||||
|             wifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL, TAG); | ||||
| 
 | ||||
|             //listen for end of video | ||||
| @@ -205,9 +211,9 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare | ||||
|                 if(action.equals(ACTION_PLAYPAUSE)) { | ||||
|                     if(mediaPlayer.isPlaying()) { | ||||
|                         mediaPlayer.pause(); | ||||
|                         note.contentView.setImageViewResource(R.id.backgroundPlayPause, R.drawable.ic_play_circle_filled_white_24dp); | ||||
|                         note.contentView.setImageViewResource(R.id.notificationPlayPause, R.drawable.ic_play_circle_filled_white_24dp); | ||||
|                         if(android.os.Build.VERSION.SDK_INT >=16){ | ||||
|                             note.bigContentView.setImageViewResource(R.id.backgroundPlayPause, R.drawable.ic_play_circle_filled_white_24dp); | ||||
|                             note.bigContentView.setImageViewResource(R.id.notificationPlayPause, R.drawable.ic_play_circle_filled_white_24dp); | ||||
|                         } | ||||
|                         noteMgr.notify(noteID, note); | ||||
|                     } | ||||
| @@ -215,9 +221,9 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare | ||||
|                         //reacquire CPU lock after auto-releasing it on pause | ||||
|                         mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK); | ||||
|                         mediaPlayer.start(); | ||||
|                         note.contentView.setImageViewResource(R.id.backgroundPlayPause, R.drawable.ic_pause_white_24dp); | ||||
|                         note.contentView.setImageViewResource(R.id.notificationPlayPause, R.drawable.ic_pause_white_24dp); | ||||
|                         if(android.os.Build.VERSION.SDK_INT >=16){ | ||||
|                             note.bigContentView.setImageViewResource(R.id.backgroundPlayPause, R.drawable.ic_pause_white_24dp); | ||||
|                             note.bigContentView.setImageViewResource(R.id.notificationPlayPause, R.drawable.ic_pause_white_24dp); | ||||
|                         } | ||||
|                         noteMgr.notify(noteID, note); | ||||
|                     } | ||||
| @@ -275,11 +281,13 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare | ||||
|             */ | ||||
| 
 | ||||
|             //build intent to return to video, on tapping notification | ||||
|             Intent openDetailView = new Intent(getApplicationContext(), | ||||
|             Intent openDetailViewIntent = new Intent(getApplicationContext(), | ||||
|                     VideoItemDetailActivity.class); | ||||
|             openDetailView.putExtra(VideoItemDetailFragment.STREAMING_SERVICE, serviceId); | ||||
|             openDetailView.putExtra(VideoItemDetailFragment.VIDEO_URL, webUrl); | ||||
|             openDetailView.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||||
|             openDetailViewIntent.putExtra(VideoItemDetailFragment.STREAMING_SERVICE, serviceId); | ||||
|             openDetailViewIntent.putExtra(VideoItemDetailFragment.VIDEO_URL, webUrl); | ||||
|             openDetailViewIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||||
|             PendingIntent openDetailView = PendingIntent.getActivity(owner, noteID, | ||||
|                     openDetailViewIntent, PendingIntent.FLAG_UPDATE_CURRENT); | ||||
| 
 | ||||
|             noteBuilder | ||||
|                     .setOngoing(true) | ||||
| @@ -291,64 +299,31 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare | ||||
|                             String.format(res.getString( | ||||
|                                     R.string.background_player_time_text), title)) | ||||
|                     .setContentIntent(PendingIntent.getActivity(getApplicationContext(), | ||||
|                             noteID, openDetailView, | ||||
|                             PendingIntent.FLAG_UPDATE_CURRENT)); | ||||
|                             noteID, openDetailViewIntent, | ||||
|                             PendingIntent.FLAG_UPDATE_CURRENT)) | ||||
|                     .setContentIntent(openDetailView); | ||||
| 
 | ||||
| 
 | ||||
|             if (android.os.Build.VERSION.SDK_INT < 21) { | ||||
| 
 | ||||
|                 NotificationCompat.Action playButton = new NotificationCompat.Action.Builder | ||||
|                         (R.drawable.ic_play_arrow_white_48dp, | ||||
|                                 res.getString(R.string.play_btn_text), playPI).build(); | ||||
| 
 | ||||
|                 noteBuilder | ||||
|                         .setContentTitle(title) | ||||
|                                 //really? Id like to put something more helpful here. | ||||
|                                 //was more of a placeholder than anything else. -medavox | ||||
|                                 //.setContentText("NewPipe is playing in the background") | ||||
|                         .setContentText(channelName) | ||||
|                                 //.setAutoCancel(!mediaPlayer.isPlaying()) | ||||
|                         .setDeleteIntent(stopPI) | ||||
|                                 //doesn't fit with Notification.MediaStyle | ||||
|                                 //.setProgress(vidLength, 0, false) | ||||
|                         .setLargeIcon(videoThumbnail) | ||||
|                         .addAction(playButton); | ||||
|                         //.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) | ||||
|                         //.setLargeIcon(cover) | ||||
| 
 | ||||
|                 //is wrapping this in an SDK version check really necessary, | ||||
|                 // if we're using NotificationCompat? | ||||
|                 // the compat libraries should handle this, right? -medavox | ||||
|                 if (android.os.Build.VERSION.SDK_INT >= 16) | ||||
|                     noteBuilder.setPriority(Notification.PRIORITY_LOW); | ||||
| 
 | ||||
|                 noteBuilder.setStyle(new NotificationCompat.MediaStyle() | ||||
|                         //.setMediaSession(mMediaSession.getSessionToken()) | ||||
|                         .setShowActionsInCompactView(new int[]{0}) | ||||
|                         .setShowCancelButton(true) | ||||
|                         .setCancelButtonIntent(stopPI)); | ||||
|                 if (videoThumbnail != null) { | ||||
|                     noteBuilder.setLargeIcon(videoThumbnail); | ||||
|                 } | ||||
|                 note = noteBuilder.build(); | ||||
|             } else { | ||||
|             RemoteViews view = | ||||
|                     new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification); | ||||
|                 view.setImageViewBitmap(R.id.backgroundCover, videoThumbnail); | ||||
|                 view.setTextViewText(R.id.backgroundSongName, title); | ||||
|                 view.setTextViewText(R.id.backgroundArtist, channelName); | ||||
|                 view.setOnClickPendingIntent(R.id.backgroundStop, stopPI); | ||||
|                 view.setOnClickPendingIntent(R.id.backgroundPlayPause, playPI); | ||||
|             view.setImageViewBitmap(R.id.notificationCover, videoThumbnail); | ||||
|             view.setTextViewText(R.id.notificationSongName, title); | ||||
|             view.setTextViewText(R.id.notificationArtist, channelName); | ||||
|             view.setOnClickPendingIntent(R.id.notificationStop, stopPI); | ||||
|             view.setOnClickPendingIntent(R.id.notificationPlayPause, playPI); | ||||
|             view.setOnClickPendingIntent(R.id.notificationContent, openDetailView); | ||||
| 
 | ||||
|             //possibly found the expandedView problem, | ||||
|             //but can't test it as I don't have a 5.0 device. -medavox | ||||
|             RemoteViews expandedView = | ||||
|                     new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification_expanded); | ||||
|                 expandedView.setImageViewBitmap(R.id.backgroundCover, videoThumbnail); | ||||
|                 expandedView.setTextViewText(R.id.backgroundSongName, title); | ||||
|                 expandedView.setTextViewText(R.id.backgroundArtist, channelName); | ||||
|                 expandedView.setOnClickPendingIntent(R.id.backgroundStop, stopPI); | ||||
|                 expandedView.setOnClickPendingIntent(R.id.backgroundPlayPause, playPI); | ||||
|                 expandedView.setImageViewBitmap(R.id.notificationCover, videoThumbnail); | ||||
|             expandedView.setTextViewText(R.id.notificationSongName, title); | ||||
|                 expandedView.setTextViewText(R.id.notificationArtist, channelName); | ||||
|             expandedView.setOnClickPendingIntent(R.id.notificationStop, stopPI); | ||||
|             expandedView.setOnClickPendingIntent(R.id.notificationPlayPause, playPI); | ||||
|             expandedView.setOnClickPendingIntent(R.id.notificationContent, openDetailView); | ||||
| 
 | ||||
| 
 | ||||
|             noteBuilder.setCategory(Notification.CATEGORY_TRANSPORT); | ||||
| 
 | ||||
| @@ -358,7 +333,7 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare | ||||
|             note = noteBuilder.build(); | ||||
|             note.contentView = view; | ||||
| 
 | ||||
|                 //todo: This never shows up. I was not able to figure out why: | ||||
|             if (android.os.Build.VERSION.SDK_INT > 16) { | ||||
|                 note.bigContentView = expandedView; | ||||
|             } | ||||
| 
 | ||||
| @@ -0,0 +1,564 @@ | ||||
| /* | ||||
|  * Copyright (C) 2014 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Extended by Christian Schabesberger on 24.12.15. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org> | ||||
|  * ExoPlayerActivity.java is part of NewPipe. all changes are under GPL3 | ||||
|  * | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * NewPipe is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| package org.schabi.newpipe.player; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.player.exoplayer.DashRendererBuilder; | ||||
| import org.schabi.newpipe.player.exoplayer.EventLogger; | ||||
| import org.schabi.newpipe.player.exoplayer.ExtractorRendererBuilder; | ||||
| import org.schabi.newpipe.player.exoplayer.HlsRendererBuilder; | ||||
| import org.schabi.newpipe.player.exoplayer.NPExoPlayer; | ||||
| import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder; | ||||
| import org.schabi.newpipe.player.exoplayer.SmoothStreamingRendererBuilder; | ||||
|  | ||||
| import com.google.android.exoplayer.AspectRatioFrameLayout; | ||||
| import com.google.android.exoplayer.ExoPlaybackException; | ||||
| import com.google.android.exoplayer.ExoPlayer; | ||||
| import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; | ||||
| import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException; | ||||
| import com.google.android.exoplayer.MediaFormat; | ||||
| import com.google.android.exoplayer.audio.AudioCapabilities; | ||||
| import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver; | ||||
| import com.google.android.exoplayer.drm.UnsupportedDrmException; | ||||
| import com.google.android.exoplayer.metadata.GeobMetadata; | ||||
| import com.google.android.exoplayer.metadata.PrivMetadata; | ||||
| import com.google.android.exoplayer.metadata.TxxxMetadata; | ||||
| import com.google.android.exoplayer.text.CaptionStyleCompat; | ||||
| import com.google.android.exoplayer.text.Cue; | ||||
| import com.google.android.exoplayer.text.SubtitleLayout; | ||||
| import com.google.android.exoplayer.util.DebugTextViewHelper; | ||||
| import com.google.android.exoplayer.util.MimeTypes; | ||||
| import com.google.android.exoplayer.util.Util; | ||||
| import com.google.android.exoplayer.util.VerboseLogUtil; | ||||
|  | ||||
| import android.Manifest.permission; | ||||
| import android.annotation.TargetApi; | ||||
| import android.app.Activity; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.content.pm.PackageManager; | ||||
| import android.net.Uri; | ||||
| import android.os.Bundle; | ||||
| import android.text.TextUtils; | ||||
| import android.util.Log; | ||||
| import android.view.KeyEvent; | ||||
| import android.view.Menu; | ||||
| import android.view.MenuItem; | ||||
| import android.view.MotionEvent; | ||||
| import android.view.SurfaceHolder; | ||||
| import android.view.SurfaceView; | ||||
| import android.view.View; | ||||
| import android.view.View.OnKeyListener; | ||||
| import android.view.View.OnTouchListener; | ||||
| import android.view.accessibility.CaptioningManager; | ||||
| import android.widget.MediaController; | ||||
| import android.widget.PopupMenu; | ||||
| import android.widget.PopupMenu.OnMenuItemClickListener; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| import java.net.CookieHandler; | ||||
| import java.net.CookieManager; | ||||
| import java.net.CookiePolicy; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
| import java.util.Map; | ||||
|  | ||||
| /** | ||||
|  * An activity that plays media using {@link NPExoPlayer}. | ||||
|  */ | ||||
| public class ExoPlayerActivity extends Activity { | ||||
|  | ||||
|     // For use within demo app code. | ||||
|     public static final String CONTENT_ID_EXTRA = "content_id"; | ||||
|     public static final String CONTENT_TYPE_EXTRA = "content_type"; | ||||
|     public static final String PROVIDER_EXTRA = "provider"; | ||||
|  | ||||
|     // For use when launching the demo app using adb. | ||||
|     private static final String CONTENT_EXT_EXTRA = "type"; | ||||
|  | ||||
|     private static final String TAG = "PlayerActivity"; | ||||
|     private static final int MENU_GROUP_TRACKS = 1; | ||||
|     private static final int ID_OFFSET = 2; | ||||
|  | ||||
|     private static final CookieManager defaultCookieManager; | ||||
|     static { | ||||
|         defaultCookieManager = new CookieManager(); | ||||
|         defaultCookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); | ||||
|     } | ||||
|  | ||||
|     private EventLogger eventLogger; | ||||
|     private MediaController mediaController; | ||||
|     private View shutterView; | ||||
|     private AspectRatioFrameLayout videoFrame; | ||||
|     private SurfaceView surfaceView; | ||||
|     private SubtitleLayout subtitleLayout; | ||||
|  | ||||
|     private NPExoPlayer player; | ||||
|     private boolean playerNeedsPrepare; | ||||
|  | ||||
|     private long playerPosition; | ||||
|     private boolean enableBackgroundAudio = true; | ||||
|  | ||||
|     private Uri contentUri; | ||||
|     private int contentType; | ||||
|     private String contentId; | ||||
|     private String provider; | ||||
|  | ||||
|     private AudioCapabilitiesReceiver audioCapabilitiesReceiver; | ||||
|  | ||||
|  | ||||
|     NPExoPlayer.Listener exoPlayerListener = new NPExoPlayer.Listener() { | ||||
|         @Override | ||||
|         public void onStateChanged(boolean playWhenReady, int playbackState) { | ||||
|             if (playbackState == ExoPlayer.STATE_ENDED) { | ||||
|                 showControls(); | ||||
|             } | ||||
|             String text = "playWhenReady=" + playWhenReady + ", playbackState="; | ||||
|             switch(playbackState) { | ||||
|                 case ExoPlayer.STATE_BUFFERING: | ||||
|                     text += "buffering"; | ||||
|                     break; | ||||
|                 case ExoPlayer.STATE_ENDED: | ||||
|                     text += "ended"; | ||||
|                     break; | ||||
|                 case ExoPlayer.STATE_IDLE: | ||||
|                     text += "idle"; | ||||
|                     break; | ||||
|                 case ExoPlayer.STATE_PREPARING: | ||||
|                     text += "preparing"; | ||||
|                     break; | ||||
|                 case ExoPlayer.STATE_READY: | ||||
|                     text += "ready"; | ||||
|                     break; | ||||
|                 default: | ||||
|                     text += "unknown"; | ||||
|                     break; | ||||
|             } | ||||
|             //todo: put text in some log | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onError(Exception e) { | ||||
|             String errorString = null; | ||||
|             if (e instanceof UnsupportedDrmException) { | ||||
|                 // Special case DRM failures. | ||||
|                 UnsupportedDrmException unsupportedDrmException = (UnsupportedDrmException) e; | ||||
|                 errorString = getString(Util.SDK_INT < 18 ? R.string.error_drm_not_supported | ||||
|                         : unsupportedDrmException.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME | ||||
|                         ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown); | ||||
|             } else if (e instanceof ExoPlaybackException | ||||
|                     && e.getCause() instanceof DecoderInitializationException) { | ||||
|                 // Special case for decoder initialization failures. | ||||
|                 DecoderInitializationException decoderInitializationException = | ||||
|                         (DecoderInitializationException) e.getCause(); | ||||
|                 if (decoderInitializationException.decoderName == null) { | ||||
|                     if (decoderInitializationException.getCause() instanceof DecoderQueryException) { | ||||
|                         errorString = getString(R.string.error_querying_decoders); | ||||
|                     } else if (decoderInitializationException.secureDecoderRequired) { | ||||
|                         errorString = getString(R.string.error_no_secure_decoder, | ||||
|                                 decoderInitializationException.mimeType); | ||||
|                     } else { | ||||
|                         errorString = getString(R.string.error_no_decoder, | ||||
|                                 decoderInitializationException.mimeType); | ||||
|                     } | ||||
|                 } else { | ||||
|                     errorString = getString(R.string.error_instantiating_decoder, | ||||
|                             decoderInitializationException.decoderName); | ||||
|                 } | ||||
|             } | ||||
|             if (errorString != null) { | ||||
|                 Toast.makeText(getApplicationContext(), errorString, Toast.LENGTH_LONG).show(); | ||||
|             } | ||||
|             playerNeedsPrepare = true; | ||||
|             showControls(); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthAspectRatio) { | ||||
|             shutterView.setVisibility(View.GONE); | ||||
|             videoFrame.setAspectRatio( | ||||
|                     height == 0 ? 1 : (width * pixelWidthAspectRatio) / height); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     SurfaceHolder.Callback surfaceHolderCallback = new SurfaceHolder.Callback() { | ||||
|         @Override | ||||
|         public void surfaceCreated(SurfaceHolder holder) { | ||||
|             if (player != null) { | ||||
|                 player.setSurface(holder.getSurface()); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { | ||||
|             // Do nothing. | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void surfaceDestroyed(SurfaceHolder holder) { | ||||
|             if (player != null) { | ||||
|                 player.blockingClearSurface(); | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     NPExoPlayer.CaptionListener captionListener = new NPExoPlayer.CaptionListener() { | ||||
|         @Override | ||||
|         public void onCues(List<Cue> cues) { | ||||
|             subtitleLayout.setCues(cues); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     NPExoPlayer.Id3MetadataListener id3MetadataListener = new NPExoPlayer.Id3MetadataListener() { | ||||
|         @Override | ||||
|         public void onId3Metadata(Map<String, Object> metadata) { | ||||
|             for (Map.Entry<String, Object> entry : metadata.entrySet()) { | ||||
|                 if (TxxxMetadata.TYPE.equals(entry.getKey())) { | ||||
|                     TxxxMetadata txxxMetadata = (TxxxMetadata) entry.getValue(); | ||||
|                     Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s, value=%s", | ||||
|                             TxxxMetadata.TYPE, txxxMetadata.description, txxxMetadata.value)); | ||||
|                 } else if (PrivMetadata.TYPE.equals(entry.getKey())) { | ||||
|                     PrivMetadata privMetadata = (PrivMetadata) entry.getValue(); | ||||
|                     Log.i(TAG, String.format("ID3 TimedMetadata %s: owner=%s", | ||||
|                             PrivMetadata.TYPE, privMetadata.owner)); | ||||
|                 } else if (GeobMetadata.TYPE.equals(entry.getKey())) { | ||||
|                     GeobMetadata geobMetadata = (GeobMetadata) entry.getValue(); | ||||
|                     Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, filename=%s, description=%s", | ||||
|                             GeobMetadata.TYPE, geobMetadata.mimeType, geobMetadata.filename, | ||||
|                             geobMetadata.description)); | ||||
|                 } else { | ||||
|                     Log.i(TAG, String.format("ID3 TimedMetadata %s", entry.getKey())); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     AudioCapabilitiesReceiver.Listener audioCapabilitiesListener = new AudioCapabilitiesReceiver.Listener() { | ||||
|         @Override | ||||
|         public void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) { | ||||
|             if (player == null) { | ||||
|                 return; | ||||
|             } | ||||
|             boolean backgrounded = player.getBackgrounded(); | ||||
|             boolean playWhenReady = player.getPlayWhenReady(); | ||||
|             releasePlayer(); | ||||
|             preparePlayer(playWhenReady); | ||||
|             player.setBackgrounded(backgrounded); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     // Activity lifecycle | ||||
|  | ||||
|     @Override | ||||
|     public void onCreate(Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|  | ||||
|         setContentView(R.layout.exo_player_activity); | ||||
|         View root = findViewById(R.id.root); | ||||
|         root.setOnTouchListener(new OnTouchListener() { | ||||
|             @Override | ||||
|             public boolean onTouch(View view, MotionEvent motionEvent) { | ||||
|                 if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { | ||||
|                     toggleControlsVisibility(); | ||||
|                 } else if (motionEvent.getAction() == MotionEvent.ACTION_UP) { | ||||
|                     view.performClick(); | ||||
|                 } | ||||
|                 return true; | ||||
|             } | ||||
|         }); | ||||
|         root.setOnKeyListener(new OnKeyListener() { | ||||
|             @Override | ||||
|             public boolean onKey(View v, int keyCode, KeyEvent event) { | ||||
|                 if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE | ||||
|                         || keyCode == KeyEvent.KEYCODE_MENU) { | ||||
|                     return false; | ||||
|                 } | ||||
|                 return mediaController.dispatchKeyEvent(event); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         shutterView = findViewById(R.id.shutter); | ||||
|  | ||||
|         videoFrame = (AspectRatioFrameLayout) findViewById(R.id.video_frame); | ||||
|         surfaceView = (SurfaceView) findViewById(R.id.surface_view); | ||||
|         surfaceView.getHolder().addCallback(surfaceHolderCallback); | ||||
|         subtitleLayout = (SubtitleLayout) findViewById(R.id.subtitles); | ||||
|  | ||||
|         //todo: replace that creapy mediaController | ||||
|         mediaController = new KeyCompatibleMediaController(this); | ||||
|         mediaController.setAnchorView(root); | ||||
|  | ||||
|         //todo: check what cookie handler does, and if we even need it | ||||
|         CookieHandler currentHandler = CookieHandler.getDefault(); | ||||
|         if (currentHandler != defaultCookieManager) { | ||||
|             CookieHandler.setDefault(defaultCookieManager); | ||||
|         } | ||||
|  | ||||
|         audioCapabilitiesReceiver = new AudioCapabilitiesReceiver(this, audioCapabilitiesListener); | ||||
|         audioCapabilitiesReceiver.register(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onNewIntent(Intent intent) { | ||||
|         releasePlayer(); | ||||
|         playerPosition = 0; | ||||
|         setIntent(intent); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onResume() { | ||||
|         super.onResume(); | ||||
|         Intent intent = getIntent(); | ||||
|         contentUri = intent.getData(); | ||||
|         contentType = intent.getIntExtra(CONTENT_TYPE_EXTRA, | ||||
|                 inferContentType(contentUri, intent.getStringExtra(CONTENT_EXT_EXTRA))); | ||||
|         contentId = intent.getStringExtra(CONTENT_ID_EXTRA); | ||||
|         provider = intent.getStringExtra(PROVIDER_EXTRA); | ||||
|         configureSubtitleView(); | ||||
|         if (player == null) { | ||||
|             if (!maybeRequestPermission()) { | ||||
|                 preparePlayer(true); | ||||
|             } | ||||
|         } else { | ||||
|             player.setBackgrounded(false); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onPause() { | ||||
|         super.onPause(); | ||||
|         if (!enableBackgroundAudio) { | ||||
|             releasePlayer(); | ||||
|         } else { | ||||
|             player.setBackgrounded(true); | ||||
|         } | ||||
|         shutterView.setVisibility(View.VISIBLE); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|         audioCapabilitiesReceiver.unregister(); | ||||
|         releasePlayer(); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     // Permission request listener method | ||||
|  | ||||
|     @Override | ||||
|     public void onRequestPermissionsResult(int requestCode, String[] permissions, | ||||
|                                            int[] grantResults) { | ||||
|         if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { | ||||
|             preparePlayer(true); | ||||
|         } else { | ||||
|             Toast.makeText(getApplicationContext(), R.string.storage_permission_denied, | ||||
|                     Toast.LENGTH_LONG).show(); | ||||
|             finish(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Permission management methods | ||||
|  | ||||
|     /** | ||||
|      * Checks whether it is necessary to ask for permission to read storage. If necessary, it also | ||||
|      * requests permission. | ||||
|      * | ||||
|      * @return true if a permission request is made. False if it is not necessary. | ||||
|      */ | ||||
|     @TargetApi(23) | ||||
|     private boolean maybeRequestPermission() { | ||||
|         if (requiresPermission(contentUri)) { | ||||
|             requestPermissions(new String[] {permission.READ_EXTERNAL_STORAGE}, 0); | ||||
|             return true; | ||||
|         } else { | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @TargetApi(23) | ||||
|     private boolean requiresPermission(Uri uri) { | ||||
|         return Util.SDK_INT >= 23 | ||||
|                 && Util.isLocalFileUri(uri) | ||||
|                 && checkSelfPermission(permission.READ_EXTERNAL_STORAGE) | ||||
|                 != PackageManager.PERMISSION_GRANTED; | ||||
|     } | ||||
|  | ||||
|     // Internal methods | ||||
|  | ||||
|     private RendererBuilder getRendererBuilder() { | ||||
|         String userAgent = Util.getUserAgent(this, "NewPipeExoPlayer"); | ||||
|         switch (contentType) { | ||||
|             case Util.TYPE_SS: | ||||
|                 // default | ||||
|                 //return new SmoothStreamingRendererBuilder(this, userAgent, contentUri.toString()); | ||||
|             case Util.TYPE_DASH: | ||||
|                 // if a dash manifest is available | ||||
|                 //return new DashRendererBuilder(this, userAgent, contentUri.toString()); | ||||
|             case Util.TYPE_HLS: | ||||
|                 // for livestreams | ||||
|                 return new HlsRendererBuilder(this, userAgent, contentUri.toString()); | ||||
|             case Util.TYPE_OTHER: | ||||
|                 // video only streaming | ||||
|                 return new ExtractorRendererBuilder(this, userAgent, contentUri); | ||||
|             default: | ||||
|                 throw new IllegalStateException("Unsupported type: " + contentType); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void preparePlayer(boolean playWhenReady) { | ||||
|         if (player == null) { | ||||
|             player = new NPExoPlayer(getRendererBuilder()); | ||||
|             player.addListener(exoPlayerListener); | ||||
|             player.setCaptionListener(captionListener); | ||||
|             player.setMetadataListener(id3MetadataListener); | ||||
|             player.seekTo(playerPosition); | ||||
|             playerNeedsPrepare = true; | ||||
|             mediaController.setMediaPlayer(player.getPlayerControl()); | ||||
|             mediaController.setEnabled(true); | ||||
|             eventLogger = new EventLogger(); | ||||
|             eventLogger.startSession(); | ||||
|             player.addListener(eventLogger); | ||||
|             player.setInfoListener(eventLogger); | ||||
|             player.setInternalErrorListener(eventLogger); | ||||
|         } | ||||
|         if (playerNeedsPrepare) { | ||||
|             player.prepare(); | ||||
|             playerNeedsPrepare = false; | ||||
|         } | ||||
|         player.setSurface(surfaceView.getHolder().getSurface()); | ||||
|         player.setPlayWhenReady(playWhenReady); | ||||
|     } | ||||
|  | ||||
|     private void releasePlayer() { | ||||
|         if (player != null) { | ||||
|             playerPosition = player.getCurrentPosition(); | ||||
|             player.release(); | ||||
|             player = null; | ||||
|             eventLogger.endSession(); | ||||
|             eventLogger = null; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void toggleControlsVisibility()  { | ||||
|         if (mediaController.isShowing()) { | ||||
|             mediaController.hide(); | ||||
|         } else { | ||||
|             showControls(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void showControls() { | ||||
|         mediaController.show(0); | ||||
|     } | ||||
|  | ||||
|     private void configureSubtitleView() { | ||||
|         CaptionStyleCompat style; | ||||
|         float fontScale; | ||||
|         if (Util.SDK_INT >= 19) { | ||||
|             style = getUserCaptionStyleV19(); | ||||
|             fontScale = getUserCaptionFontScaleV19(); | ||||
|         } else { | ||||
|             style = CaptionStyleCompat.DEFAULT; | ||||
|             fontScale = 1.0f; | ||||
|         } | ||||
|         subtitleLayout.setStyle(style); | ||||
|         subtitleLayout.setFractionalTextSize(SubtitleLayout.DEFAULT_TEXT_SIZE_FRACTION * fontScale); | ||||
|     } | ||||
|  | ||||
|     @TargetApi(19) | ||||
|     private float getUserCaptionFontScaleV19() { | ||||
|         CaptioningManager captioningManager = | ||||
|                 (CaptioningManager) getSystemService(Context.CAPTIONING_SERVICE); | ||||
|         return captioningManager.getFontScale(); | ||||
|     } | ||||
|  | ||||
|     @TargetApi(19) | ||||
|     private CaptionStyleCompat getUserCaptionStyleV19() { | ||||
|         CaptioningManager captioningManager = | ||||
|                 (CaptioningManager) getSystemService(Context.CAPTIONING_SERVICE); | ||||
|         return CaptionStyleCompat.createFromCaptionStyle(captioningManager.getUserStyle()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Makes a best guess to infer the type from a media {@link Uri} and an optional overriding file | ||||
|      * extension. | ||||
|      * | ||||
|      * @param uri The {@link Uri} of the media. | ||||
|      * @param fileExtension An overriding file extension. | ||||
|      * @return The inferred type. | ||||
|      */ | ||||
|     private static int inferContentType(Uri uri, String fileExtension) { | ||||
|         String lastPathSegment = !TextUtils.isEmpty(fileExtension) ? "." + fileExtension | ||||
|                 : uri.getLastPathSegment(); | ||||
|         return Util.inferContentType(lastPathSegment); | ||||
|     } | ||||
|  | ||||
|     private static final class KeyCompatibleMediaController extends MediaController { | ||||
|  | ||||
|         private MediaController.MediaPlayerControl playerControl; | ||||
|  | ||||
|         public KeyCompatibleMediaController(Context context) { | ||||
|             super(context); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void setMediaPlayer(MediaController.MediaPlayerControl playerControl) { | ||||
|             super.setMediaPlayer(playerControl); | ||||
|             this.playerControl = playerControl; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public boolean dispatchKeyEvent(KeyEvent event) { | ||||
|             int keyCode = event.getKeyCode(); | ||||
|             if (playerControl.canSeekForward() && keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { | ||||
|                 if (event.getAction() == KeyEvent.ACTION_DOWN) { | ||||
|                     playerControl.seekTo(playerControl.getCurrentPosition() + 15000); // milliseconds | ||||
|                     show(); | ||||
|                 } | ||||
|                 return true; | ||||
|             } else if (playerControl.canSeekBackward() && keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) { | ||||
|                 if (event.getAction() == KeyEvent.ACTION_DOWN) { | ||||
|                     playerControl.seekTo(playerControl.getCurrentPosition() - 5000); // milliseconds | ||||
|                     show(); | ||||
|                 } | ||||
|                 return true; | ||||
|             } | ||||
|             return super.dispatchKeyEvent(event); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,10 +1,11 @@ | ||||
| package org.schabi.newpipe; | ||||
| package org.schabi.newpipe.player; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.content.SharedPreferences; | ||||
| import android.content.pm.ActivityInfo; | ||||
| import android.content.res.Configuration; | ||||
| import android.graphics.drawable.Drawable; | ||||
| import android.media.MediaPlayer; | ||||
| import android.media.AudioManager; | ||||
| import android.net.Uri; | ||||
| @@ -27,6 +28,9 @@ import android.widget.MediaController; | ||||
| import android.widget.ProgressBar; | ||||
| import android.widget.VideoView; | ||||
| 
 | ||||
| import org.schabi.newpipe.App; | ||||
| import org.schabi.newpipe.R; | ||||
| 
 | ||||
| /** | ||||
|  * Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org> | ||||
|  * PlayVideoActivity.java is part of NewPipe. | ||||
| @@ -80,6 +84,10 @@ public class PlayVideoActivity extends AppCompatActivity { | ||||
| 
 | ||||
|         setContentView(R.layout.activity_play_video); | ||||
|         setVolumeControlStream(AudioManager.STREAM_MUSIC); | ||||
| 
 | ||||
|         //set background arrow style | ||||
|         getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_arrow_back_white_24dp); | ||||
| 
 | ||||
|         isLandscape = checkIfLandscape(); | ||||
|         hasSoftKeys = checkIfHasSoftKeys(); | ||||
| 
 | ||||
| @@ -191,7 +199,6 @@ public class PlayVideoActivity extends AppCompatActivity { | ||||
|     @Override | ||||
|     public void onResume() { | ||||
|         super.onResume(); | ||||
|         App.checkStartTor(this); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
| @@ -320,7 +327,7 @@ public class PlayVideoActivity extends AppCompatActivity { | ||||
| 
 | ||||
|             int realHeight = realDisplayMetrics.heightPixels; | ||||
|             int displayHeight = displayMetrics.heightPixels; | ||||
|             return (realHeight - displayHeight); | ||||
|             return realHeight - displayHeight; | ||||
|         } else { | ||||
|             return 50; | ||||
|         } | ||||
| @@ -337,7 +344,7 @@ public class PlayVideoActivity extends AppCompatActivity { | ||||
| 
 | ||||
|             int realWidth = realDisplayMetrics.widthPixels; | ||||
|             int displayWidth = displayMetrics.widthPixels; | ||||
|             return (realWidth - displayWidth); | ||||
|             return realWidth - displayWidth; | ||||
|         } else { | ||||
|             return 50; | ||||
|         } | ||||
| @@ -0,0 +1,268 @@ | ||||
| /* | ||||
|  * Copyright (C) 2014 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| package org.schabi.newpipe.player.exoplayer; | ||||
|  | ||||
| import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder; | ||||
|  | ||||
| import com.google.android.exoplayer.DefaultLoadControl; | ||||
| import com.google.android.exoplayer.LoadControl; | ||||
| import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; | ||||
| import com.google.android.exoplayer.MediaCodecSelector; | ||||
| import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; | ||||
| import com.google.android.exoplayer.TrackRenderer; | ||||
| import com.google.android.exoplayer.audio.AudioCapabilities; | ||||
| import com.google.android.exoplayer.chunk.ChunkSampleSource; | ||||
| import com.google.android.exoplayer.chunk.ChunkSource; | ||||
| import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator; | ||||
| import com.google.android.exoplayer.dash.DashChunkSource; | ||||
| import com.google.android.exoplayer.dash.DefaultDashTrackSelector; | ||||
| import com.google.android.exoplayer.dash.mpd.AdaptationSet; | ||||
| import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; | ||||
| import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser; | ||||
| import com.google.android.exoplayer.dash.mpd.Period; | ||||
| import com.google.android.exoplayer.dash.mpd.UtcTimingElement; | ||||
| import com.google.android.exoplayer.dash.mpd.UtcTimingElementResolver; | ||||
| import com.google.android.exoplayer.dash.mpd.UtcTimingElementResolver.UtcTimingCallback; | ||||
| import com.google.android.exoplayer.drm.MediaDrmCallback; | ||||
| import com.google.android.exoplayer.drm.StreamingDrmSessionManager; | ||||
| import com.google.android.exoplayer.drm.UnsupportedDrmException; | ||||
| import com.google.android.exoplayer.text.TextTrackRenderer; | ||||
| import com.google.android.exoplayer.upstream.DataSource; | ||||
| import com.google.android.exoplayer.upstream.DefaultAllocator; | ||||
| import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; | ||||
| import com.google.android.exoplayer.upstream.DefaultUriDataSource; | ||||
| import com.google.android.exoplayer.upstream.UriDataSource; | ||||
| import com.google.android.exoplayer.util.ManifestFetcher; | ||||
| import com.google.android.exoplayer.util.Util; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.media.AudioManager; | ||||
| import android.media.MediaCodec; | ||||
| import android.os.Handler; | ||||
| import android.util.Log; | ||||
|  | ||||
| import java.io.IOException; | ||||
|  | ||||
| /** | ||||
|  * A {@link RendererBuilder} for DASH. | ||||
|  */ | ||||
| public class DashRendererBuilder implements RendererBuilder { | ||||
|  | ||||
|   private static final String TAG = "DashRendererBuilder"; | ||||
|  | ||||
|   private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; | ||||
|   private static final int VIDEO_BUFFER_SEGMENTS = 200; | ||||
|   private static final int AUDIO_BUFFER_SEGMENTS = 54; | ||||
|   private static final int TEXT_BUFFER_SEGMENTS = 2; | ||||
|   private static final int LIVE_EDGE_LATENCY_MS = 30000; | ||||
|  | ||||
|   private static final int SECURITY_LEVEL_UNKNOWN = -1; | ||||
|   private static final int SECURITY_LEVEL_1 = 1; | ||||
|   private static final int SECURITY_LEVEL_3 = 3; | ||||
|  | ||||
|   private final Context context; | ||||
|   private final String userAgent; | ||||
|   private final String url; | ||||
|   private final MediaDrmCallback drmCallback; | ||||
|  | ||||
|   private AsyncRendererBuilder currentAsyncBuilder; | ||||
|  | ||||
|   public DashRendererBuilder(Context context, String userAgent, String url, | ||||
|       MediaDrmCallback drmCallback) { | ||||
|     this.context = context; | ||||
|     this.userAgent = userAgent; | ||||
|     this.url = url; | ||||
|     this.drmCallback = drmCallback; | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void buildRenderers(NPExoPlayer player) { | ||||
|     currentAsyncBuilder = new AsyncRendererBuilder(context, userAgent, url, drmCallback, player); | ||||
|     currentAsyncBuilder.init(); | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void cancel() { | ||||
|     if (currentAsyncBuilder != null) { | ||||
|       currentAsyncBuilder.cancel(); | ||||
|       currentAsyncBuilder = null; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private static final class AsyncRendererBuilder | ||||
|       implements ManifestFetcher.ManifestCallback<MediaPresentationDescription>, UtcTimingCallback { | ||||
|  | ||||
|     private final Context context; | ||||
|     private final String userAgent; | ||||
|     private final MediaDrmCallback drmCallback; | ||||
|     private final NPExoPlayer player; | ||||
|     private final ManifestFetcher<MediaPresentationDescription> manifestFetcher; | ||||
|     private final UriDataSource manifestDataSource; | ||||
|  | ||||
|     private boolean canceled; | ||||
|     private MediaPresentationDescription manifest; | ||||
|     private long elapsedRealtimeOffset; | ||||
|  | ||||
|     public AsyncRendererBuilder(Context context, String userAgent, String url, | ||||
|         MediaDrmCallback drmCallback, NPExoPlayer player) { | ||||
|       this.context = context; | ||||
|       this.userAgent = userAgent; | ||||
|       this.drmCallback = drmCallback; | ||||
|       this.player = player; | ||||
|       MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser(); | ||||
|       manifestDataSource = new DefaultUriDataSource(context, userAgent); | ||||
|       manifestFetcher = new ManifestFetcher<>(url, manifestDataSource, parser); | ||||
|     } | ||||
|  | ||||
|     public void init() { | ||||
|       manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this); | ||||
|     } | ||||
|  | ||||
|     public void cancel() { | ||||
|       canceled = true; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onSingleManifest(MediaPresentationDescription manifest) { | ||||
|       if (canceled) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       this.manifest = manifest; | ||||
|       if (manifest.dynamic && manifest.utcTiming != null) { | ||||
|         UtcTimingElementResolver.resolveTimingElement(manifestDataSource, manifest.utcTiming, | ||||
|             manifestFetcher.getManifestLoadCompleteTimestamp(), this); | ||||
|       } else { | ||||
|         buildRenderers(); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onSingleManifestError(IOException e) { | ||||
|       if (canceled) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       player.onRenderersError(e); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onTimestampResolved(UtcTimingElement utcTiming, long elapsedRealtimeOffset) { | ||||
|       if (canceled) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       this.elapsedRealtimeOffset = elapsedRealtimeOffset; | ||||
|       buildRenderers(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onTimestampError(UtcTimingElement utcTiming, IOException e) { | ||||
|       if (canceled) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       Log.e(TAG, "Failed to resolve UtcTiming element [" + utcTiming + "]", e); | ||||
|       // Be optimistic and continue in the hope that the device clock is correct. | ||||
|       buildRenderers(); | ||||
|     } | ||||
|  | ||||
|     private void buildRenderers() { | ||||
|       Period period = manifest.getPeriod(0); | ||||
|       Handler mainHandler = player.getMainHandler(); | ||||
|       LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE)); | ||||
|       DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player); | ||||
|  | ||||
|       boolean hasContentProtection = false; | ||||
|       for (int i = 0; i < period.adaptationSets.size(); i++) { | ||||
|         AdaptationSet adaptationSet = period.adaptationSets.get(i); | ||||
|         if (adaptationSet.type != AdaptationSet.TYPE_UNKNOWN) { | ||||
|           hasContentProtection |= adaptationSet.hasContentProtection(); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // Check drm support if necessary. | ||||
|       boolean filterHdContent = false; | ||||
|       StreamingDrmSessionManager drmSessionManager = null; | ||||
|       if (hasContentProtection) { | ||||
|         if (Util.SDK_INT < 18) { | ||||
|           player.onRenderersError( | ||||
|               new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME)); | ||||
|           return; | ||||
|         } | ||||
|         try { | ||||
|           drmSessionManager = StreamingDrmSessionManager.newWidevineInstance( | ||||
|               player.getPlaybackLooper(), drmCallback, null, player.getMainHandler(), player); | ||||
|           filterHdContent = getWidevineSecurityLevel(drmSessionManager) != SECURITY_LEVEL_1; | ||||
|         } catch (UnsupportedDrmException e) { | ||||
|           player.onRenderersError(e); | ||||
|           return; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // Build the video renderer. | ||||
|       DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); | ||||
|       ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, | ||||
|           DefaultDashTrackSelector.newVideoInstance(context, true, filterHdContent), | ||||
|           videoDataSource, new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS, | ||||
|           elapsedRealtimeOffset, mainHandler, player, NPExoPlayer.TYPE_VIDEO); | ||||
|       ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, | ||||
|           VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, | ||||
|           NPExoPlayer.TYPE_VIDEO); | ||||
|       TrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, videoSampleSource, | ||||
|           MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, | ||||
|           drmSessionManager, true, mainHandler, player, 50); | ||||
|  | ||||
|       // Build the audio renderer. | ||||
|       DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); | ||||
|       ChunkSource audioChunkSource = new DashChunkSource(manifestFetcher, | ||||
|           DefaultDashTrackSelector.newAudioInstance(), audioDataSource, null, LIVE_EDGE_LATENCY_MS, | ||||
|           elapsedRealtimeOffset, mainHandler, player, NPExoPlayer.TYPE_AUDIO); | ||||
|       ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, | ||||
|           AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, | ||||
|           NPExoPlayer.TYPE_AUDIO); | ||||
|       TrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource, | ||||
|           MediaCodecSelector.DEFAULT, drmSessionManager, true, mainHandler, player, | ||||
|           AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC); | ||||
|  | ||||
|       // Build the text renderer. | ||||
|       DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); | ||||
|       ChunkSource textChunkSource = new DashChunkSource(manifestFetcher, | ||||
|           DefaultDashTrackSelector.newTextInstance(), textDataSource, null, LIVE_EDGE_LATENCY_MS, | ||||
|           elapsedRealtimeOffset, mainHandler, player, NPExoPlayer.TYPE_TEXT); | ||||
|       ChunkSampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl, | ||||
|           TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, | ||||
|           NPExoPlayer.TYPE_TEXT); | ||||
|       TrackRenderer textRenderer = new TextTrackRenderer(textSampleSource, player, | ||||
|           mainHandler.getLooper()); | ||||
|  | ||||
|       // Invoke the callback. | ||||
|       TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT]; | ||||
|       renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer; | ||||
|       renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer; | ||||
|       renderers[NPExoPlayer.TYPE_TEXT] = textRenderer; | ||||
|       player.onRenderers(renderers, bandwidthMeter); | ||||
|     } | ||||
|  | ||||
|     private static int getWidevineSecurityLevel(StreamingDrmSessionManager sessionManager) { | ||||
|       String securityLevelProperty = sessionManager.getPropertyString("securityLevel"); | ||||
|       return securityLevelProperty.equals("L1") ? SECURITY_LEVEL_1 : securityLevelProperty | ||||
|           .equals("L3") ? SECURITY_LEVEL_3 : SECURITY_LEVEL_UNKNOWN; | ||||
|     } | ||||
|  | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,214 @@ | ||||
| /* | ||||
|  * Copyright (C) 2014 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| package org.schabi.newpipe.player.exoplayer; | ||||
|  | ||||
| import com.google.android.exoplayer.ExoPlayer; | ||||
| import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; | ||||
| import com.google.android.exoplayer.TimeRange; | ||||
| import com.google.android.exoplayer.audio.AudioTrack; | ||||
| import com.google.android.exoplayer.chunk.Format; | ||||
| import com.google.android.exoplayer.util.VerboseLogUtil; | ||||
|  | ||||
| import android.media.MediaCodec.CryptoException; | ||||
| import android.os.SystemClock; | ||||
| import android.util.Log; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.text.NumberFormat; | ||||
| import java.util.Locale; | ||||
|  | ||||
| /** | ||||
|  * Logs player events using {@link Log}. | ||||
|  */ | ||||
| public class EventLogger implements NPExoPlayer.Listener, NPExoPlayer.InfoListener, | ||||
|     NPExoPlayer.InternalErrorListener { | ||||
|  | ||||
|   private static final String TAG = "EventLogger"; | ||||
|   private static final NumberFormat TIME_FORMAT; | ||||
|   static { | ||||
|     TIME_FORMAT = NumberFormat.getInstance(Locale.US); | ||||
|     TIME_FORMAT.setMinimumFractionDigits(2); | ||||
|     TIME_FORMAT.setMaximumFractionDigits(2); | ||||
|   } | ||||
|  | ||||
|   private long sessionStartTimeMs; | ||||
|   private long[] loadStartTimeMs; | ||||
|   private long[] availableRangeValuesUs; | ||||
|  | ||||
|   public EventLogger() { | ||||
|     loadStartTimeMs = new long[NPExoPlayer.RENDERER_COUNT]; | ||||
|   } | ||||
|  | ||||
|   public void startSession() { | ||||
|     sessionStartTimeMs = SystemClock.elapsedRealtime(); | ||||
|     Log.d(TAG, "start [0]"); | ||||
|   } | ||||
|  | ||||
|   public void endSession() { | ||||
|     Log.d(TAG, "end [" + getSessionTimeString() + "]"); | ||||
|   } | ||||
|  | ||||
|   // NPExoPlayer.Listener | ||||
|  | ||||
|   @Override | ||||
|   public void onStateChanged(boolean playWhenReady, int state) { | ||||
|     Log.d(TAG, "state [" + getSessionTimeString() + ", " + playWhenReady + ", " | ||||
|         + getStateString(state) + "]"); | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onError(Exception e) { | ||||
|     Log.e(TAG, "playerFailed [" + getSessionTimeString() + "]", e); | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, | ||||
|       float pixelWidthHeightRatio) { | ||||
|     Log.d(TAG, "videoSizeChanged [" + width + ", " + height + ", " + unappliedRotationDegrees | ||||
|         + ", " + pixelWidthHeightRatio + "]"); | ||||
|   } | ||||
|  | ||||
|   // NPExoPlayer.InfoListener | ||||
|  | ||||
|   @Override | ||||
|   public void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate) { | ||||
|     Log.d(TAG, "bandwidth [" + getSessionTimeString() + ", " + bytes + ", " | ||||
|         + getTimeString(elapsedMs) + ", " + bitrateEstimate + "]"); | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onDroppedFrames(int count, long elapsed) { | ||||
|     Log.d(TAG, "droppedFrames [" + getSessionTimeString() + ", " + count + "]"); | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onLoadStarted(int sourceId, long length, int type, int trigger, Format format, | ||||
|       long mediaStartTimeMs, long mediaEndTimeMs) { | ||||
|     loadStartTimeMs[sourceId] = SystemClock.elapsedRealtime(); | ||||
|     if (VerboseLogUtil.isTagEnabled(TAG)) { | ||||
|       Log.v(TAG, "loadStart [" + getSessionTimeString() + ", " + sourceId + ", " + type | ||||
|           + ", " + mediaStartTimeMs + ", " + mediaEndTimeMs + "]"); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format, | ||||
|        long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs) { | ||||
|     if (VerboseLogUtil.isTagEnabled(TAG)) { | ||||
|       long downloadTime = SystemClock.elapsedRealtime() - loadStartTimeMs[sourceId]; | ||||
|       Log.v(TAG, "loadEnd [" + getSessionTimeString() + ", " + sourceId + ", " + downloadTime | ||||
|           + "]"); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onVideoFormatEnabled(Format format, int trigger, long mediaTimeMs) { | ||||
|     Log.d(TAG, "videoFormat [" + getSessionTimeString() + ", " + format.id + ", " | ||||
|         + Integer.toString(trigger) + "]"); | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onAudioFormatEnabled(Format format, int trigger, long mediaTimeMs) { | ||||
|     Log.d(TAG, "audioFormat [" + getSessionTimeString() + ", " + format.id + ", " | ||||
|         + Integer.toString(trigger) + "]"); | ||||
|   } | ||||
|  | ||||
|   // NPExoPlayer.InternalErrorListener | ||||
|  | ||||
|   @Override | ||||
|   public void onLoadError(int sourceId, IOException e) { | ||||
|     printInternalError("loadError", e); | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onRendererInitializationError(Exception e) { | ||||
|     printInternalError("rendererInitError", e); | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onDrmSessionManagerError(Exception e) { | ||||
|     printInternalError("drmSessionManagerError", e); | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onDecoderInitializationError(DecoderInitializationException e) { | ||||
|     printInternalError("decoderInitializationError", e); | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onAudioTrackInitializationError(AudioTrack.InitializationException e) { | ||||
|     printInternalError("audioTrackInitializationError", e); | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onAudioTrackWriteError(AudioTrack.WriteException e) { | ||||
|     printInternalError("audioTrackWriteError", e); | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { | ||||
|     printInternalError("audioTrackUnderrun [" + bufferSize + ", " + bufferSizeMs + ", " | ||||
|         + elapsedSinceLastFeedMs + "]", null); | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onCryptoError(CryptoException e) { | ||||
|     printInternalError("cryptoError", e); | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs, | ||||
|       long initializationDurationMs) { | ||||
|     Log.d(TAG, "decoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]"); | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onAvailableRangeChanged(int sourceId, TimeRange availableRange) { | ||||
|     availableRangeValuesUs = availableRange.getCurrentBoundsUs(availableRangeValuesUs); | ||||
|     Log.d(TAG, "availableRange [" + availableRange.isStatic() + ", " + availableRangeValuesUs[0] | ||||
|         + ", " + availableRangeValuesUs[1] + "]"); | ||||
|   } | ||||
|  | ||||
|   private void printInternalError(String type, Exception e) { | ||||
|     Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e); | ||||
|   } | ||||
|  | ||||
|   private String getStateString(int state) { | ||||
|     switch (state) { | ||||
|       case ExoPlayer.STATE_BUFFERING: | ||||
|         return "B"; | ||||
|       case ExoPlayer.STATE_ENDED: | ||||
|         return "E"; | ||||
|       case ExoPlayer.STATE_IDLE: | ||||
|         return "I"; | ||||
|       case ExoPlayer.STATE_PREPARING: | ||||
|         return "P"; | ||||
|       case ExoPlayer.STATE_READY: | ||||
|         return "R"; | ||||
|       default: | ||||
|         return "?"; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private String getSessionTimeString() { | ||||
|     return getTimeString(SystemClock.elapsedRealtime() - sessionStartTimeMs); | ||||
|   } | ||||
|  | ||||
|   private String getTimeString(long timeMs) { | ||||
|     return TIME_FORMAT.format((timeMs) / 1000f); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,89 @@ | ||||
| /* | ||||
|  * Copyright (C) 2014 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| package org.schabi.newpipe.player.exoplayer; | ||||
|  | ||||
| import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder; | ||||
|  | ||||
| import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; | ||||
| import com.google.android.exoplayer.MediaCodecSelector; | ||||
| import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; | ||||
| import com.google.android.exoplayer.TrackRenderer; | ||||
| import com.google.android.exoplayer.audio.AudioCapabilities; | ||||
| import com.google.android.exoplayer.extractor.Extractor; | ||||
| import com.google.android.exoplayer.extractor.ExtractorSampleSource; | ||||
| import com.google.android.exoplayer.text.TextTrackRenderer; | ||||
| import com.google.android.exoplayer.upstream.Allocator; | ||||
| import com.google.android.exoplayer.upstream.DataSource; | ||||
| import com.google.android.exoplayer.upstream.DefaultAllocator; | ||||
| import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; | ||||
| import com.google.android.exoplayer.upstream.DefaultUriDataSource; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.media.AudioManager; | ||||
| import android.media.MediaCodec; | ||||
| import android.net.Uri; | ||||
|  | ||||
| /** | ||||
|  * A {@link RendererBuilder} for streams that can be read using an {@link Extractor}. | ||||
|  */ | ||||
| public class ExtractorRendererBuilder implements RendererBuilder { | ||||
|  | ||||
|   private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; | ||||
|   private static final int BUFFER_SEGMENT_COUNT = 256; | ||||
|  | ||||
|   private final Context context; | ||||
|   private final String userAgent; | ||||
|   private final Uri uri; | ||||
|  | ||||
|   public ExtractorRendererBuilder(Context context, String userAgent, Uri uri) { | ||||
|     this.context = context; | ||||
|     this.userAgent = userAgent; | ||||
|     this.uri = uri; | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void buildRenderers(NPExoPlayer player) { | ||||
|     Allocator allocator = new DefaultAllocator(BUFFER_SEGMENT_SIZE); | ||||
|  | ||||
|     // Build the video and audio renderers. | ||||
|     DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(player.getMainHandler(), | ||||
|         null); | ||||
|     DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); | ||||
|     ExtractorSampleSource sampleSource = new ExtractorSampleSource(uri, dataSource, allocator, | ||||
|         BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE); | ||||
|     MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, | ||||
|         sampleSource, MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, | ||||
|         player.getMainHandler(), player, 50); | ||||
|     MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource, | ||||
|         MediaCodecSelector.DEFAULT, null, true, player.getMainHandler(), player, | ||||
|         AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC); | ||||
|     TrackRenderer textRenderer = new TextTrackRenderer(sampleSource, player, | ||||
|         player.getMainHandler().getLooper()); | ||||
|  | ||||
|     // Invoke the callback. | ||||
|     TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT]; | ||||
|     renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer; | ||||
|     renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer; | ||||
|     renderers[NPExoPlayer.TYPE_TEXT] = textRenderer; | ||||
|     player.onRenderers(renderers, bandwidthMeter); | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void cancel() { | ||||
|     // Do nothing. | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,180 @@ | ||||
| /* | ||||
|  * Copyright (C) 2014 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| package org.schabi.newpipe.player.exoplayer; | ||||
|  | ||||
| import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder; | ||||
|  | ||||
| import com.google.android.exoplayer.DefaultLoadControl; | ||||
| import com.google.android.exoplayer.LoadControl; | ||||
| import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; | ||||
| import com.google.android.exoplayer.MediaCodecSelector; | ||||
| import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; | ||||
| import com.google.android.exoplayer.TrackRenderer; | ||||
| import com.google.android.exoplayer.audio.AudioCapabilities; | ||||
| import com.google.android.exoplayer.hls.DefaultHlsTrackSelector; | ||||
| import com.google.android.exoplayer.hls.HlsChunkSource; | ||||
| import com.google.android.exoplayer.hls.HlsMasterPlaylist; | ||||
| import com.google.android.exoplayer.hls.HlsPlaylist; | ||||
| import com.google.android.exoplayer.hls.HlsPlaylistParser; | ||||
| import com.google.android.exoplayer.hls.HlsSampleSource; | ||||
| import com.google.android.exoplayer.hls.PtsTimestampAdjusterProvider; | ||||
| import com.google.android.exoplayer.metadata.Id3Parser; | ||||
| import com.google.android.exoplayer.metadata.MetadataTrackRenderer; | ||||
| import com.google.android.exoplayer.text.TextTrackRenderer; | ||||
| import com.google.android.exoplayer.text.eia608.Eia608TrackRenderer; | ||||
| import com.google.android.exoplayer.upstream.DataSource; | ||||
| import com.google.android.exoplayer.upstream.DefaultAllocator; | ||||
| import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; | ||||
| import com.google.android.exoplayer.upstream.DefaultUriDataSource; | ||||
| import com.google.android.exoplayer.util.ManifestFetcher; | ||||
| import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.media.AudioManager; | ||||
| import android.media.MediaCodec; | ||||
| import android.os.Handler; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.util.Map; | ||||
|  | ||||
| /** | ||||
|  * A {@link RendererBuilder} for HLS. | ||||
|  */ | ||||
| public class HlsRendererBuilder implements RendererBuilder { | ||||
|  | ||||
|   private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; | ||||
|   private static final int MAIN_BUFFER_SEGMENTS = 256; | ||||
|   private static final int TEXT_BUFFER_SEGMENTS = 2; | ||||
|  | ||||
|   private final Context context; | ||||
|   private final String userAgent; | ||||
|   private final String url; | ||||
|  | ||||
|   private AsyncRendererBuilder currentAsyncBuilder; | ||||
|  | ||||
|   public HlsRendererBuilder(Context context, String userAgent, String url) { | ||||
|     this.context = context; | ||||
|     this.userAgent = userAgent; | ||||
|     this.url = url; | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void buildRenderers(NPExoPlayer player) { | ||||
|     currentAsyncBuilder = new AsyncRendererBuilder(context, userAgent, url, player); | ||||
|     currentAsyncBuilder.init(); | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void cancel() { | ||||
|     if (currentAsyncBuilder != null) { | ||||
|       currentAsyncBuilder.cancel(); | ||||
|       currentAsyncBuilder = null; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private static final class AsyncRendererBuilder implements ManifestCallback<HlsPlaylist> { | ||||
|  | ||||
|     private final Context context; | ||||
|     private final String userAgent; | ||||
|     private final String url; | ||||
|     private final NPExoPlayer player; | ||||
|     private final ManifestFetcher<HlsPlaylist> playlistFetcher; | ||||
|  | ||||
|     private boolean canceled; | ||||
|  | ||||
|     public AsyncRendererBuilder(Context context, String userAgent, String url, NPExoPlayer player) { | ||||
|       this.context = context; | ||||
|       this.userAgent = userAgent; | ||||
|       this.url = url; | ||||
|       this.player = player; | ||||
|       HlsPlaylistParser parser = new HlsPlaylistParser(); | ||||
|       playlistFetcher = new ManifestFetcher<>(url, new DefaultUriDataSource(context, userAgent), | ||||
|           parser); | ||||
|     } | ||||
|  | ||||
|     public void init() { | ||||
|       playlistFetcher.singleLoad(player.getMainHandler().getLooper(), this); | ||||
|     } | ||||
|  | ||||
|     public void cancel() { | ||||
|       canceled = true; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onSingleManifestError(IOException e) { | ||||
|       if (canceled) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       player.onRenderersError(e); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onSingleManifest(HlsPlaylist manifest) { | ||||
|       if (canceled) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       Handler mainHandler = player.getMainHandler(); | ||||
|       LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE)); | ||||
|       DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); | ||||
|       PtsTimestampAdjusterProvider timestampAdjusterProvider = new PtsTimestampAdjusterProvider(); | ||||
|  | ||||
|       // Build the video/audio/metadata renderers. | ||||
|       DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); | ||||
|       HlsChunkSource chunkSource = new HlsChunkSource(true /* isMaster */, dataSource, url, | ||||
|           manifest, DefaultHlsTrackSelector.newDefaultInstance(context), bandwidthMeter, | ||||
|           timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE); | ||||
|       HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, loadControl, | ||||
|           MAIN_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, NPExoPlayer.TYPE_VIDEO); | ||||
|       MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, | ||||
|           sampleSource, MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, | ||||
|           5000, mainHandler, player, 50); | ||||
|       MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource, | ||||
|           MediaCodecSelector.DEFAULT, null, true, player.getMainHandler(), player, | ||||
|           AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC); | ||||
|       MetadataTrackRenderer<Map<String, Object>> id3Renderer = new MetadataTrackRenderer<>( | ||||
|           sampleSource, new Id3Parser(), player, mainHandler.getLooper()); | ||||
|  | ||||
|       // Build the text renderer, preferring Webvtt where available. | ||||
|       boolean preferWebvtt = false; | ||||
|       if (manifest instanceof HlsMasterPlaylist) { | ||||
|         preferWebvtt = !((HlsMasterPlaylist) manifest).subtitles.isEmpty(); | ||||
|       } | ||||
|       TrackRenderer textRenderer; | ||||
|       if (preferWebvtt) { | ||||
|         DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); | ||||
|         HlsChunkSource textChunkSource = new HlsChunkSource(false /* isMaster */, textDataSource, | ||||
|             url, manifest, DefaultHlsTrackSelector.newVttInstance(), bandwidthMeter, | ||||
|             timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE); | ||||
|         HlsSampleSource textSampleSource = new HlsSampleSource(textChunkSource, loadControl, | ||||
|             TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, NPExoPlayer.TYPE_TEXT); | ||||
|         textRenderer = new TextTrackRenderer(textSampleSource, player, mainHandler.getLooper()); | ||||
|       } else { | ||||
|         textRenderer = new Eia608TrackRenderer(sampleSource, player, mainHandler.getLooper()); | ||||
|       } | ||||
|  | ||||
|       TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT]; | ||||
|       renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer; | ||||
|       renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer; | ||||
|       renderers[NPExoPlayer.TYPE_METADATA] = id3Renderer; | ||||
|       renderers[NPExoPlayer.TYPE_TEXT] = textRenderer; | ||||
|       player.onRenderers(renderers, bandwidthMeter); | ||||
|     } | ||||
|  | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,599 @@ | ||||
| /* | ||||
|  * Copyright (C) 2014 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| package org.schabi.newpipe.player.exoplayer; | ||||
|  | ||||
| import com.google.android.exoplayer.CodecCounters; | ||||
| import com.google.android.exoplayer.DummyTrackRenderer; | ||||
| import com.google.android.exoplayer.ExoPlaybackException; | ||||
| import com.google.android.exoplayer.ExoPlayer; | ||||
| import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; | ||||
| import com.google.android.exoplayer.MediaCodecTrackRenderer; | ||||
| import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; | ||||
| import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; | ||||
| import com.google.android.exoplayer.MediaFormat; | ||||
| import com.google.android.exoplayer.TimeRange; | ||||
| import com.google.android.exoplayer.TrackRenderer; | ||||
| import com.google.android.exoplayer.audio.AudioTrack; | ||||
| import com.google.android.exoplayer.chunk.ChunkSampleSource; | ||||
| import com.google.android.exoplayer.chunk.Format; | ||||
| import com.google.android.exoplayer.dash.DashChunkSource; | ||||
| import com.google.android.exoplayer.drm.StreamingDrmSessionManager; | ||||
| import com.google.android.exoplayer.hls.HlsSampleSource; | ||||
| import com.google.android.exoplayer.metadata.MetadataTrackRenderer.MetadataRenderer; | ||||
| import com.google.android.exoplayer.text.Cue; | ||||
| import com.google.android.exoplayer.text.TextRenderer; | ||||
| import com.google.android.exoplayer.upstream.BandwidthMeter; | ||||
| import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; | ||||
| import com.google.android.exoplayer.util.DebugTextViewHelper; | ||||
| import com.google.android.exoplayer.util.PlayerControl; | ||||
|  | ||||
| import android.media.MediaCodec.CryptoException; | ||||
| import android.os.Handler; | ||||
| import android.os.Looper; | ||||
| import android.view.Surface; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.concurrent.CopyOnWriteArrayList; | ||||
|  | ||||
| /** | ||||
|  * A wrapper around {@link ExoPlayer} that provides a higher level interface. It can be prepared | ||||
|  * with one of a number of {@link RendererBuilder} classes to suit different use cases (e.g. DASH, | ||||
|  * SmoothStreaming and so on). | ||||
|  */ | ||||
| public class NPExoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener, | ||||
|     HlsSampleSource.EventListener, DefaultBandwidthMeter.EventListener, | ||||
|     MediaCodecVideoTrackRenderer.EventListener, MediaCodecAudioTrackRenderer.EventListener, | ||||
|     StreamingDrmSessionManager.EventListener, DashChunkSource.EventListener, TextRenderer, | ||||
|     MetadataRenderer<Map<String, Object>>, DebugTextViewHelper.Provider { | ||||
|  | ||||
|   /** | ||||
|    * Builds renderers for the player. | ||||
|    */ | ||||
|   public interface RendererBuilder { | ||||
|     /** | ||||
|      * Builds renderers for playback. | ||||
|      * | ||||
|      * @param player The player for which renderers are being built. {@link NPExoPlayer#onRenderers} | ||||
|      *     should be invoked once the renderers have been built. If building fails, | ||||
|      *     {@link NPExoPlayer#onRenderersError} should be invoked. | ||||
|      */ | ||||
|     void buildRenderers(NPExoPlayer player); | ||||
|     /** | ||||
|      * Cancels the current build operation, if there is one. Else does nothing. | ||||
|      * <p> | ||||
|      * A canceled build operation must not invoke {@link NPExoPlayer#onRenderers} or | ||||
|      * {@link NPExoPlayer#onRenderersError} on the player, which may have been released. | ||||
|      */ | ||||
|     void cancel(); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * A listener for core events. | ||||
|    */ | ||||
|   public interface Listener { | ||||
|     void onStateChanged(boolean playWhenReady, int playbackState); | ||||
|     void onError(Exception e); | ||||
|     void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, | ||||
|         float pixelWidthHeightRatio); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * A listener for internal errors. | ||||
|    * <p> | ||||
|    * These errors are not visible to the user, and hence this listener is provided for | ||||
|    * informational purposes only. Note however that an internal error may cause a fatal | ||||
|    * error if the player fails to recover. If this happens, {@link Listener#onError(Exception)} | ||||
|    * will be invoked. | ||||
|    */ | ||||
|   public interface InternalErrorListener { | ||||
|     void onRendererInitializationError(Exception e); | ||||
|     void onAudioTrackInitializationError(AudioTrack.InitializationException e); | ||||
|     void onAudioTrackWriteError(AudioTrack.WriteException e); | ||||
|     void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs); | ||||
|     void onDecoderInitializationError(DecoderInitializationException e); | ||||
|     void onCryptoError(CryptoException e); | ||||
|     void onLoadError(int sourceId, IOException e); | ||||
|     void onDrmSessionManagerError(Exception e); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * A listener for debugging information. | ||||
|    */ | ||||
|   public interface InfoListener { | ||||
|     void onVideoFormatEnabled(Format format, int trigger, long mediaTimeMs); | ||||
|     void onAudioFormatEnabled(Format format, int trigger, long mediaTimeMs); | ||||
|     void onDroppedFrames(int count, long elapsed); | ||||
|     void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate); | ||||
|     void onLoadStarted(int sourceId, long length, int type, int trigger, Format format, | ||||
|         long mediaStartTimeMs, long mediaEndTimeMs); | ||||
|     void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format, | ||||
|         long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs); | ||||
|     void onDecoderInitialized(String decoderName, long elapsedRealtimeMs, | ||||
|         long initializationDurationMs); | ||||
|     void onAvailableRangeChanged(int sourceId, TimeRange availableRange); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * A listener for receiving notifications of timed text. | ||||
|    */ | ||||
|   public interface CaptionListener { | ||||
|     void onCues(List<Cue> cues); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * A listener for receiving ID3 metadata parsed from the media stream. | ||||
|    */ | ||||
|   public interface Id3MetadataListener { | ||||
|     void onId3Metadata(Map<String, Object> metadata); | ||||
|   } | ||||
|  | ||||
|   // Constants pulled into this class for convenience. | ||||
|   public static final int STATE_IDLE = ExoPlayer.STATE_IDLE; | ||||
|   public static final int STATE_PREPARING = ExoPlayer.STATE_PREPARING; | ||||
|   public static final int STATE_BUFFERING = ExoPlayer.STATE_BUFFERING; | ||||
|   public static final int STATE_READY = ExoPlayer.STATE_READY; | ||||
|   public static final int STATE_ENDED = ExoPlayer.STATE_ENDED; | ||||
|   public static final int TRACK_DISABLED = ExoPlayer.TRACK_DISABLED; | ||||
|   public static final int TRACK_DEFAULT = ExoPlayer.TRACK_DEFAULT; | ||||
|  | ||||
|   public static final int RENDERER_COUNT = 4; | ||||
|   public static final int TYPE_VIDEO = 0; | ||||
|   public static final int TYPE_AUDIO = 1; | ||||
|   public static final int TYPE_TEXT = 2; | ||||
|   public static final int TYPE_METADATA = 3; | ||||
|  | ||||
|   private static final int RENDERER_BUILDING_STATE_IDLE = 1; | ||||
|   private static final int RENDERER_BUILDING_STATE_BUILDING = 2; | ||||
|   private static final int RENDERER_BUILDING_STATE_BUILT = 3; | ||||
|  | ||||
|   private final RendererBuilder rendererBuilder; | ||||
|   private final ExoPlayer player; | ||||
|   private final PlayerControl playerControl; | ||||
|   private final Handler mainHandler; | ||||
|   private final CopyOnWriteArrayList<Listener> listeners; | ||||
|  | ||||
|   private int rendererBuildingState; | ||||
|   private int lastReportedPlaybackState; | ||||
|   private boolean lastReportedPlayWhenReady; | ||||
|  | ||||
|   private Surface surface; | ||||
|   private TrackRenderer videoRenderer; | ||||
|   private CodecCounters codecCounters; | ||||
|   private Format videoFormat; | ||||
|   private int videoTrackToRestore; | ||||
|  | ||||
|   private BandwidthMeter bandwidthMeter; | ||||
|   private boolean backgrounded; | ||||
|  | ||||
|   private CaptionListener captionListener; | ||||
|   private Id3MetadataListener id3MetadataListener; | ||||
|   private InternalErrorListener internalErrorListener; | ||||
|   private InfoListener infoListener; | ||||
|  | ||||
|   public NPExoPlayer(RendererBuilder rendererBuilder) { | ||||
|     this.rendererBuilder = rendererBuilder; | ||||
|     player = ExoPlayer.Factory.newInstance(RENDERER_COUNT, 1000, 5000); | ||||
|     player.addListener(this); | ||||
|     playerControl = new PlayerControl(player); | ||||
|     mainHandler = new Handler(); | ||||
|     listeners = new CopyOnWriteArrayList<>(); | ||||
|     lastReportedPlaybackState = STATE_IDLE; | ||||
|     rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; | ||||
|     // Disable text initially. | ||||
|     player.setSelectedTrack(TYPE_TEXT, TRACK_DISABLED); | ||||
|   } | ||||
|  | ||||
|   public PlayerControl getPlayerControl() { | ||||
|     return playerControl; | ||||
|   } | ||||
|  | ||||
|   public void addListener(Listener listener) { | ||||
|     listeners.add(listener); | ||||
|   } | ||||
|  | ||||
|   public void removeListener(Listener listener) { | ||||
|     listeners.remove(listener); | ||||
|   } | ||||
|  | ||||
|   public void setInternalErrorListener(InternalErrorListener listener) { | ||||
|     internalErrorListener = listener; | ||||
|   } | ||||
|  | ||||
|   public void setInfoListener(InfoListener listener) { | ||||
|     infoListener = listener; | ||||
|   } | ||||
|  | ||||
|   public void setCaptionListener(CaptionListener listener) { | ||||
|     captionListener = listener; | ||||
|   } | ||||
|  | ||||
|   public void setMetadataListener(Id3MetadataListener listener) { | ||||
|     id3MetadataListener = listener; | ||||
|   } | ||||
|  | ||||
|   public void setSurface(Surface surface) { | ||||
|     this.surface = surface; | ||||
|     pushSurface(false); | ||||
|   } | ||||
|  | ||||
|   public Surface getSurface() { | ||||
|     return surface; | ||||
|   } | ||||
|  | ||||
|   public void blockingClearSurface() { | ||||
|     surface = null; | ||||
|     pushSurface(true); | ||||
|   } | ||||
|  | ||||
|   public int getTrackCount(int type) { | ||||
|     return player.getTrackCount(type); | ||||
|   } | ||||
|  | ||||
|   public MediaFormat getTrackFormat(int type, int index) { | ||||
|     return player.getTrackFormat(type, index); | ||||
|   } | ||||
|  | ||||
|   public int getSelectedTrack(int type) { | ||||
|     return player.getSelectedTrack(type); | ||||
|   } | ||||
|  | ||||
|   public void setSelectedTrack(int type, int index) { | ||||
|     player.setSelectedTrack(type, index); | ||||
|     if (type == TYPE_TEXT && index < 0 && captionListener != null) { | ||||
|       captionListener.onCues(Collections.<Cue>emptyList()); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public boolean getBackgrounded() { | ||||
|     return backgrounded; | ||||
|   } | ||||
|  | ||||
|   public void setBackgrounded(boolean backgrounded) { | ||||
|     if (this.backgrounded == backgrounded) { | ||||
|       return; | ||||
|     } | ||||
|     this.backgrounded = backgrounded; | ||||
|     if (backgrounded) { | ||||
|       videoTrackToRestore = getSelectedTrack(TYPE_VIDEO); | ||||
|       setSelectedTrack(TYPE_VIDEO, TRACK_DISABLED); | ||||
|       blockingClearSurface(); | ||||
|     } else { | ||||
|       setSelectedTrack(TYPE_VIDEO, videoTrackToRestore); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public void prepare() { | ||||
|     if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT) { | ||||
|       player.stop(); | ||||
|     } | ||||
|     rendererBuilder.cancel(); | ||||
|     videoFormat = null; | ||||
|     videoRenderer = null; | ||||
|     rendererBuildingState = RENDERER_BUILDING_STATE_BUILDING; | ||||
|     maybeReportPlayerState(); | ||||
|     rendererBuilder.buildRenderers(this); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Invoked with the results from a {@link RendererBuilder}. | ||||
|    * | ||||
|    * @param renderers Renderers indexed by {@link NPExoPlayer} TYPE_* constants. An individual | ||||
|    *     element may be null if there do not exist tracks of the corresponding type. | ||||
|    * @param bandwidthMeter Provides an estimate of the currently available bandwidth. May be null. | ||||
|    */ | ||||
|   /* package */ void onRenderers(TrackRenderer[] renderers, BandwidthMeter bandwidthMeter) { | ||||
|     for (int i = 0; i < RENDERER_COUNT; i++) { | ||||
|       if (renderers[i] == null) { | ||||
|         // Convert a null renderer to a dummy renderer. | ||||
|         renderers[i] = new DummyTrackRenderer(); | ||||
|       } | ||||
|     } | ||||
|     // Complete preparation. | ||||
|     this.videoRenderer = renderers[TYPE_VIDEO]; | ||||
|     this.codecCounters = videoRenderer instanceof MediaCodecTrackRenderer | ||||
|         ? ((MediaCodecTrackRenderer) videoRenderer).codecCounters | ||||
|         : renderers[TYPE_AUDIO] instanceof MediaCodecTrackRenderer | ||||
|         ? ((MediaCodecTrackRenderer) renderers[TYPE_AUDIO]).codecCounters : null; | ||||
|     this.bandwidthMeter = bandwidthMeter; | ||||
|     pushSurface(false); | ||||
|     player.prepare(renderers); | ||||
|     rendererBuildingState = RENDERER_BUILDING_STATE_BUILT; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Invoked if a {@link RendererBuilder} encounters an error. | ||||
|    * | ||||
|    * @param e Describes the error. | ||||
|    */ | ||||
|   /* package */ void onRenderersError(Exception e) { | ||||
|     if (internalErrorListener != null) { | ||||
|       internalErrorListener.onRendererInitializationError(e); | ||||
|     } | ||||
|     for (Listener listener : listeners) { | ||||
|       listener.onError(e); | ||||
|     } | ||||
|     rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; | ||||
|     maybeReportPlayerState(); | ||||
|   } | ||||
|  | ||||
|   public void setPlayWhenReady(boolean playWhenReady) { | ||||
|     player.setPlayWhenReady(playWhenReady); | ||||
|   } | ||||
|  | ||||
|   public void seekTo(long positionMs) { | ||||
|     player.seekTo(positionMs); | ||||
|   } | ||||
|  | ||||
|   public void release() { | ||||
|     rendererBuilder.cancel(); | ||||
|     rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; | ||||
|     surface = null; | ||||
|     player.release(); | ||||
|   } | ||||
|  | ||||
|   public int getPlaybackState() { | ||||
|     if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILDING) { | ||||
|       return STATE_PREPARING; | ||||
|     } | ||||
|     int playerState = player.getPlaybackState(); | ||||
|     if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT && playerState == STATE_IDLE) { | ||||
|       // This is an edge case where the renderers are built, but are still being passed to the | ||||
|       // player's playback thread. | ||||
|       return STATE_PREPARING; | ||||
|     } | ||||
|     return playerState; | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public Format getFormat() { | ||||
|     return videoFormat; | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public BandwidthMeter getBandwidthMeter() { | ||||
|     return bandwidthMeter; | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public CodecCounters getCodecCounters() { | ||||
|     return codecCounters; | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public long getCurrentPosition() { | ||||
|     return player.getCurrentPosition(); | ||||
|   } | ||||
|  | ||||
|   public long getDuration() { | ||||
|     return player.getDuration(); | ||||
|   } | ||||
|  | ||||
|   public int getBufferedPercentage() { | ||||
|     return player.getBufferedPercentage(); | ||||
|   } | ||||
|  | ||||
|   public boolean getPlayWhenReady() { | ||||
|     return player.getPlayWhenReady(); | ||||
|   } | ||||
|  | ||||
|   /* package */ Looper getPlaybackLooper() { | ||||
|     return player.getPlaybackLooper(); | ||||
|   } | ||||
|  | ||||
|   /* package */ Handler getMainHandler() { | ||||
|     return mainHandler; | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onPlayerStateChanged(boolean playWhenReady, int state) { | ||||
|     maybeReportPlayerState(); | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onPlayerError(ExoPlaybackException exception) { | ||||
|     rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; | ||||
|     for (Listener listener : listeners) { | ||||
|       listener.onError(exception); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, | ||||
|       float pixelWidthHeightRatio) { | ||||
|     for (Listener listener : listeners) { | ||||
|       listener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onDroppedFrames(int count, long elapsed) { | ||||
|     if (infoListener != null) { | ||||
|       infoListener.onDroppedFrames(count, elapsed); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate) { | ||||
|     if (infoListener != null) { | ||||
|       infoListener.onBandwidthSample(elapsedMs, bytes, bitrateEstimate); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onDownstreamFormatChanged(int sourceId, Format format, int trigger, | ||||
|       long mediaTimeMs) { | ||||
|     if (infoListener == null) { | ||||
|       return; | ||||
|     } | ||||
|     if (sourceId == TYPE_VIDEO) { | ||||
|       videoFormat = format; | ||||
|       infoListener.onVideoFormatEnabled(format, trigger, mediaTimeMs); | ||||
|     } else if (sourceId == TYPE_AUDIO) { | ||||
|       infoListener.onAudioFormatEnabled(format, trigger, mediaTimeMs); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onDrmKeysLoaded() { | ||||
|     // Do nothing. | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onDrmSessionManagerError(Exception e) { | ||||
|     if (internalErrorListener != null) { | ||||
|       internalErrorListener.onDrmSessionManagerError(e); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onDecoderInitializationError(DecoderInitializationException e) { | ||||
|     if (internalErrorListener != null) { | ||||
|       internalErrorListener.onDecoderInitializationError(e); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onAudioTrackInitializationError(AudioTrack.InitializationException e) { | ||||
|     if (internalErrorListener != null) { | ||||
|       internalErrorListener.onAudioTrackInitializationError(e); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onAudioTrackWriteError(AudioTrack.WriteException e) { | ||||
|     if (internalErrorListener != null) { | ||||
|       internalErrorListener.onAudioTrackWriteError(e); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { | ||||
|     if (internalErrorListener != null) { | ||||
|       internalErrorListener.onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onCryptoError(CryptoException e) { | ||||
|     if (internalErrorListener != null) { | ||||
|       internalErrorListener.onCryptoError(e); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs, | ||||
|       long initializationDurationMs) { | ||||
|     if (infoListener != null) { | ||||
|       infoListener.onDecoderInitialized(decoderName, elapsedRealtimeMs, initializationDurationMs); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onLoadError(int sourceId, IOException e) { | ||||
|     if (internalErrorListener != null) { | ||||
|       internalErrorListener.onLoadError(sourceId, e); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onCues(List<Cue> cues) { | ||||
|     if (captionListener != null && getSelectedTrack(TYPE_TEXT) != TRACK_DISABLED) { | ||||
|       captionListener.onCues(cues); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onMetadata(Map<String, Object> metadata) { | ||||
|     if (id3MetadataListener != null && getSelectedTrack(TYPE_METADATA) != TRACK_DISABLED) { | ||||
|       id3MetadataListener.onId3Metadata(metadata); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onAvailableRangeChanged(int sourceId, TimeRange availableRange) { | ||||
|     if (infoListener != null) { | ||||
|       infoListener.onAvailableRangeChanged(sourceId, availableRange); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onPlayWhenReadyCommitted() { | ||||
|     // Do nothing. | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onDrawnToSurface(Surface surface) { | ||||
|     // Do nothing. | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onLoadStarted(int sourceId, long length, int type, int trigger, Format format, | ||||
|       long mediaStartTimeMs, long mediaEndTimeMs) { | ||||
|     if (infoListener != null) { | ||||
|       infoListener.onLoadStarted(sourceId, length, type, trigger, format, mediaStartTimeMs, | ||||
|           mediaEndTimeMs); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format, | ||||
|       long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs) { | ||||
|     if (infoListener != null) { | ||||
|       infoListener.onLoadCompleted(sourceId, bytesLoaded, type, trigger, format, mediaStartTimeMs, | ||||
|           mediaEndTimeMs, elapsedRealtimeMs, loadDurationMs); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onLoadCanceled(int sourceId, long bytesLoaded) { | ||||
|     // Do nothing. | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void onUpstreamDiscarded(int sourceId, long mediaStartTimeMs, long mediaEndTimeMs) { | ||||
|     // Do nothing. | ||||
|   } | ||||
|  | ||||
|   private void maybeReportPlayerState() { | ||||
|     boolean playWhenReady = player.getPlayWhenReady(); | ||||
|     int playbackState = getPlaybackState(); | ||||
|     if (lastReportedPlayWhenReady != playWhenReady || lastReportedPlaybackState != playbackState) { | ||||
|       for (Listener listener : listeners) { | ||||
|         listener.onStateChanged(playWhenReady, playbackState); | ||||
|       } | ||||
|       lastReportedPlayWhenReady = playWhenReady; | ||||
|       lastReportedPlaybackState = playbackState; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private void pushSurface(boolean blockForSurfacePush) { | ||||
|     if (videoRenderer == null) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     if (blockForSurfacePush) { | ||||
|       player.blockingSendMessage( | ||||
|           videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface); | ||||
|     } else { | ||||
|       player.sendMessage( | ||||
|           videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,206 @@ | ||||
| /* | ||||
|  * Copyright (C) 2014 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| package org.schabi.newpipe.player.exoplayer; | ||||
|  | ||||
|  | ||||
| import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder; | ||||
|  | ||||
| import com.google.android.exoplayer.DefaultLoadControl; | ||||
| import com.google.android.exoplayer.LoadControl; | ||||
| import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; | ||||
| import com.google.android.exoplayer.MediaCodecSelector; | ||||
| import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; | ||||
| import com.google.android.exoplayer.TrackRenderer; | ||||
| import com.google.android.exoplayer.audio.AudioCapabilities; | ||||
| import com.google.android.exoplayer.chunk.ChunkSampleSource; | ||||
| import com.google.android.exoplayer.chunk.ChunkSource; | ||||
| import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator; | ||||
| import com.google.android.exoplayer.drm.DrmSessionManager; | ||||
| import com.google.android.exoplayer.drm.MediaDrmCallback; | ||||
| import com.google.android.exoplayer.drm.StreamingDrmSessionManager; | ||||
| import com.google.android.exoplayer.drm.UnsupportedDrmException; | ||||
| import com.google.android.exoplayer.smoothstreaming.DefaultSmoothStreamingTrackSelector; | ||||
| import com.google.android.exoplayer.smoothstreaming.SmoothStreamingChunkSource; | ||||
| import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest; | ||||
| import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifestParser; | ||||
| import com.google.android.exoplayer.text.TextTrackRenderer; | ||||
| import com.google.android.exoplayer.upstream.DataSource; | ||||
| import com.google.android.exoplayer.upstream.DefaultAllocator; | ||||
| import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; | ||||
| import com.google.android.exoplayer.upstream.DefaultHttpDataSource; | ||||
| import com.google.android.exoplayer.upstream.DefaultUriDataSource; | ||||
| import com.google.android.exoplayer.util.ManifestFetcher; | ||||
| import com.google.android.exoplayer.util.Util; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.media.AudioManager; | ||||
| import android.media.MediaCodec; | ||||
| import android.os.Handler; | ||||
|  | ||||
| import java.io.IOException; | ||||
|  | ||||
| /** | ||||
|  * A {@link RendererBuilder} for SmoothStreaming. | ||||
|  */ | ||||
| public class SmoothStreamingRendererBuilder implements RendererBuilder { | ||||
|  | ||||
|   private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; | ||||
|   private static final int VIDEO_BUFFER_SEGMENTS = 200; | ||||
|   private static final int AUDIO_BUFFER_SEGMENTS = 54; | ||||
|   private static final int TEXT_BUFFER_SEGMENTS = 2; | ||||
|   private static final int LIVE_EDGE_LATENCY_MS = 30000; | ||||
|  | ||||
|   private final Context context; | ||||
|   private final String userAgent; | ||||
|   private final String url; | ||||
|   private final MediaDrmCallback drmCallback; | ||||
|  | ||||
|   private AsyncRendererBuilder currentAsyncBuilder; | ||||
|  | ||||
|   public SmoothStreamingRendererBuilder(Context context, String userAgent, String url, | ||||
|       MediaDrmCallback drmCallback) { | ||||
|     this.context = context; | ||||
|     this.userAgent = userAgent; | ||||
|     this.url = Util.toLowerInvariant(url).endsWith("/manifest") ? url : url + "/Manifest"; | ||||
|     this.drmCallback = drmCallback; | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void buildRenderers(NPExoPlayer player) { | ||||
|     currentAsyncBuilder = new AsyncRendererBuilder(context, userAgent, url, drmCallback, player); | ||||
|     currentAsyncBuilder.init(); | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public void cancel() { | ||||
|     if (currentAsyncBuilder != null) { | ||||
|       currentAsyncBuilder.cancel(); | ||||
|       currentAsyncBuilder = null; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private static final class AsyncRendererBuilder | ||||
|       implements ManifestFetcher.ManifestCallback<SmoothStreamingManifest> { | ||||
|  | ||||
|     private final Context context; | ||||
|     private final String userAgent; | ||||
|     private final MediaDrmCallback drmCallback; | ||||
|     private final NPExoPlayer player; | ||||
|     private final ManifestFetcher<SmoothStreamingManifest> manifestFetcher; | ||||
|  | ||||
|     private boolean canceled; | ||||
|  | ||||
|     public AsyncRendererBuilder(Context context, String userAgent, String url, | ||||
|         MediaDrmCallback drmCallback, NPExoPlayer player) { | ||||
|       this.context = context; | ||||
|       this.userAgent = userAgent; | ||||
|       this.drmCallback = drmCallback; | ||||
|       this.player = player; | ||||
|       SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser(); | ||||
|       manifestFetcher = new ManifestFetcher<>(url, new DefaultHttpDataSource(userAgent, null), | ||||
|           parser); | ||||
|     } | ||||
|  | ||||
|     public void init() { | ||||
|       manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this); | ||||
|     } | ||||
|  | ||||
|     public void cancel() { | ||||
|       canceled = true; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onSingleManifestError(IOException exception) { | ||||
|       if (canceled) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       player.onRenderersError(exception); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onSingleManifest(SmoothStreamingManifest manifest) { | ||||
|       if (canceled) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       Handler mainHandler = player.getMainHandler(); | ||||
|       LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE)); | ||||
|       DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player); | ||||
|  | ||||
|       // Check drm support if necessary. | ||||
|       DrmSessionManager drmSessionManager = null; | ||||
|       if (manifest.protectionElement != null) { | ||||
|         if (Util.SDK_INT < 18) { | ||||
|           player.onRenderersError( | ||||
|               new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME)); | ||||
|           return; | ||||
|         } | ||||
|         try { | ||||
|           drmSessionManager = new StreamingDrmSessionManager(manifest.protectionElement.uuid, | ||||
|               player.getPlaybackLooper(), drmCallback, null, player.getMainHandler(), player); | ||||
|         } catch (UnsupportedDrmException e) { | ||||
|           player.onRenderersError(e); | ||||
|           return; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // Build the video renderer. | ||||
|       DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); | ||||
|       ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifestFetcher, | ||||
|           DefaultSmoothStreamingTrackSelector.newVideoInstance(context, true, false), | ||||
|           videoDataSource, new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS); | ||||
|       ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, | ||||
|           VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, | ||||
|           NPExoPlayer.TYPE_VIDEO); | ||||
|       TrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, videoSampleSource, | ||||
|           MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, | ||||
|           drmSessionManager, true, mainHandler, player, 50); | ||||
|  | ||||
|       // Build the audio renderer. | ||||
|       DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); | ||||
|       ChunkSource audioChunkSource = new SmoothStreamingChunkSource(manifestFetcher, | ||||
|           DefaultSmoothStreamingTrackSelector.newAudioInstance(), | ||||
|           audioDataSource, null, LIVE_EDGE_LATENCY_MS); | ||||
|       ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, | ||||
|           AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, | ||||
|           NPExoPlayer.TYPE_AUDIO); | ||||
|       TrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource, | ||||
|           MediaCodecSelector.DEFAULT, drmSessionManager, true, mainHandler, player, | ||||
|           AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC); | ||||
|  | ||||
|       // Build the text renderer. | ||||
|       DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); | ||||
|       ChunkSource textChunkSource = new SmoothStreamingChunkSource(manifestFetcher, | ||||
|           DefaultSmoothStreamingTrackSelector.newTextInstance(), | ||||
|           textDataSource, null, LIVE_EDGE_LATENCY_MS); | ||||
|       ChunkSampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl, | ||||
|           TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, | ||||
|           NPExoPlayer.TYPE_TEXT); | ||||
|       TrackRenderer textRenderer = new TextTrackRenderer(textSampleSource, player, | ||||
|           mainHandler.getLooper()); | ||||
|  | ||||
|       // Invoke the callback. | ||||
|       TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT]; | ||||
|       renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer; | ||||
|       renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer; | ||||
|       renderers[NPExoPlayer.TYPE_TEXT] = textRenderer; | ||||
|       player.onRenderers(renderers, bandwidthMeter); | ||||
|     } | ||||
|  | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_arrow_back_white_24dp.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 148 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_play_circle_transparent.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.1 KiB | 
| After Width: | Height: | Size: 149 B | 
| After Width: | Height: | Size: 119 B | 
| After Width: | Height: | Size: 140 B | 
| After Width: | Height: | Size: 195 B | 
| After Width: | Height: | Size: 200 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-mdpi/ic_arrow_back_white_24dp.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 115 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-mdpi/ic_play_circle_transparent.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xhdpi/ic_arrow_back_white_24dp.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 131 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xhdpi/ic_play_circle_transparent.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_arrow_back_white_24dp.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 191 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_play_circle_transparent.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxxhdpi/ic_arrow_back_white_24dp.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 194 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxxhdpi/ic_play_circle_transparent.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 6.2 KiB | 
							
								
								
									
										279
									
								
								app/src/main/res/layout-v18/fragment_videoitem_detail.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,279 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
|  | ||||
| <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     tools:context=".VideoItemDetailFragment" | ||||
|     android:textIsSelectable="true" | ||||
|     style="?android:attr/textAppearanceLarge" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:id="@+id/videoitem_detail"> | ||||
|  | ||||
|     <com.nirhart.parallaxscroll.views.ParallaxScrollView | ||||
|         android:id="@+id/detailMainContent" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent" | ||||
|         android:visibility="visible" | ||||
|         app:parallax_factor="1.9" | ||||
|         tools:ignore="UselessParent"> | ||||
|  | ||||
|         <RelativeLayout | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content"> | ||||
|  | ||||
|             <RelativeLayout | ||||
|                 android:id="@+id/detailVideoThumbnailWindowLayout" | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:background="?attr/selectableItemBackground"> | ||||
|  | ||||
|                 <ImageView android:id="@+id/detailThumbnailView" | ||||
|                     android:contentDescription="@string/detail_thumbnail_view_description" | ||||
|                     android:layout_width="match_parent" | ||||
|                     android:layout_height="wrap_content" | ||||
|                     android:scaleType="fitCenter" | ||||
|                     android:adjustViewBounds="true" | ||||
|                     android:layout_alignParentLeft="true" | ||||
|                     android:layout_alignParentStart="true" | ||||
|                     android:layout_alignParentTop="true" | ||||
|                     android:background="@android:color/black" | ||||
|                     android:src="@drawable/dummy_thumbnail_dark"/> | ||||
|  | ||||
|                 <ProgressBar android:id="@+id/detailProgressBar" | ||||
|                     android:layout_width="wrap_content" | ||||
|                     android:layout_height="wrap_content" | ||||
|                     android:layout_centerInParent="true" | ||||
|                     android:indeterminate="true"/> | ||||
|  | ||||
|                 <ImageView android:id="@+id/playArrowView" | ||||
|                     android:layout_width="wrap_content" | ||||
|                     android:layout_height="wrap_content" | ||||
|                     android:background="@android:color/transparent" | ||||
|                     android:layout_centerInParent="true" | ||||
|                     android:src="@drawable/ic_play_circle_transparent" | ||||
|                     android:visibility="invisible"/> | ||||
|  | ||||
|                 <Button | ||||
|                     android:id="@+id/detailVideoThumbnailWindowBackgroundButton" | ||||
|                     android:layout_width="match_parent" | ||||
|                     android:layout_height="match_parent" | ||||
|                     android:background="?attr/selectableItemBackground"/> | ||||
|  | ||||
|             </RelativeLayout> | ||||
|  | ||||
|             <RelativeLayout android:id="@+id/detailTextContentLayout" | ||||
|  | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="match_parent" | ||||
|                 android:layout_below="@id/detailVideoThumbnailWindowLayout" | ||||
|                 android:background="@color/light_background_color" | ||||
|                 android:visibility="gone"> | ||||
|  | ||||
|                 <LinearLayout | ||||
|                     android:layout_width="match_parent" | ||||
|                     android:layout_height="wrap_content" | ||||
|                     android:id="@+id/detailTopView"> | ||||
|  | ||||
|                     <TextView android:id="@+id/detailVideoTitleView" | ||||
|                         android:layout_width="0dp" | ||||
|                         android:layout_weight=".7" | ||||
|                         android:layout_height="wrap_content" | ||||
|                         android:textSize="@dimen/video_item_detail_title_text_size" | ||||
|                         android:textAppearance="?android:attr/textAppearanceLarge" | ||||
|                         android:layout_marginTop="12dp" | ||||
|                         android:layout_marginLeft="12dp" | ||||
|                         android:layout_marginStart="12dp" | ||||
|                         android:text="Title"/> | ||||
|  | ||||
|                     <ImageView | ||||
|                         android:layout_width="15dp" | ||||
|                         android:layout_height="30dp" | ||||
|                         android:id="@+id/toggleDescriptionView" | ||||
|                         android:src="@drawable/arrow_down" | ||||
|                         android:layout_marginLeft="10dp" | ||||
|                         android:layout_marginStart="10dp" | ||||
|                         android:layout_marginRight="10dp" | ||||
|                         android:layout_marginEnd="10dp" | ||||
|                         android:layout_marginTop="8dp"/> | ||||
|  | ||||
|                 </LinearLayout> | ||||
|  | ||||
|                 <TextView android:id="@+id/detailViewCountView" | ||||
|                     android:layout_width="match_parent" | ||||
|                     android:layout_height="wrap_content" | ||||
|                     android:textSize="@dimen/video_item_detail_views_text_size" | ||||
|                     android:textAppearance="?android:attr/textAppearanceLarge" | ||||
|                     android:layout_marginLeft="12dp" | ||||
|                     android:layout_marginStart="12dp" | ||||
|                     android:text="10,069,948 views" | ||||
|                     android:layout_below="@id/detailTopView" | ||||
|                     android:layout_alignParentLeft="true" | ||||
|                     android:layout_alignParentStart="true" | ||||
|                     android:layout_marginTop="5dp" /> | ||||
|  | ||||
|                 <RelativeLayout | ||||
|                     android:layout_width="match_parent" | ||||
|                     android:layout_height="wrap_content" | ||||
|                     android:layout_below="@id/detailViewCountView" | ||||
|                     android:id="@+id/detailExtraView" | ||||
|                     android:layout_marginLeft="12dp" | ||||
|                     android:layout_marginStart="12dp" | ||||
|                     android:layout_marginRight="12dp" | ||||
|                     android:layout_marginEnd="12dp" | ||||
|                     android:visibility="gone"> | ||||
|                     <TextView android:id="@+id/detailUploadDateView" | ||||
|                         android:layout_width="wrap_content" | ||||
|                         android:layout_height="wrap_content" | ||||
|                         android:textSize="@dimen/video_item_detail_upload_date_text_size" | ||||
|                         android:textAppearance="?android:attr/textAppearanceMedium" | ||||
|                         android:text="Upload date" | ||||
|                         android:layout_marginTop="3dp" /> | ||||
|  | ||||
|                     <TextView android:id="@+id/detailDescriptionView" | ||||
|                         android:layout_width="match_parent" | ||||
|                         android:layout_height="wrap_content" | ||||
|                         android:textSize="@dimen/video_item_detail_description_text_size" | ||||
|                         android:textAppearance="?android:attr/textAppearanceMedium" | ||||
|                         android:layout_below="@id/detailUploadDateView" | ||||
|                         android:text="Description............." | ||||
|                         android:layout_marginTop="3dp" /> | ||||
|                 </RelativeLayout> | ||||
|  | ||||
|                 <LinearLayout | ||||
|                     android:layout_width="match_parent" | ||||
|                     android:layout_height="wrap_content" | ||||
|                     android:id="@+id/linearLayout" | ||||
|                     android:layout_marginLeft="12dp" | ||||
|                     android:layout_marginStart="12dp" | ||||
|                     android:layout_below="@+id/detailExtraView" | ||||
|                     android:layout_alignParentRight="true" | ||||
|                     android:layout_alignParentEnd="true" | ||||
|                     android:layout_marginTop="5dp"> | ||||
|                     <ImageView android:id="@+id/detailThumbsUpImgView" | ||||
|                         android:contentDescription="@string/detail_likes_img_view_description" | ||||
|                         android:layout_width="@dimen/video_item_detail_like_image_width" | ||||
|                         android:layout_height="@dimen/video_item_detail_like_image_height" | ||||
|                         android:src="@drawable/thumbs_up" /> | ||||
|  | ||||
|                     <TextView android:id="@+id/detailThumbsUpCountView" | ||||
|                         android:layout_width="wrap_content" | ||||
|                         android:layout_height="wrap_content" | ||||
|                         android:textSize="@dimen/video_item_detail_likes_text_size" | ||||
|                         android:textAppearance="?android:attr/textAppearanceMedium" | ||||
|                         android:text="200" /> | ||||
|  | ||||
|                     <ImageView android:id="@+id/detailThumbsDownImgView" | ||||
|                         android:contentDescription="@string/detail_dislikes_img_view_description" | ||||
|                         android:layout_width="@dimen/video_item_detail_like_image_width" | ||||
|                         android:layout_height="@dimen/video_item_detail_like_image_height" | ||||
|                         android:src="@drawable/thumbs_down" | ||||
|                         android:layout_marginLeft="10dp" | ||||
|                         android:layout_marginStart="10dp"/> | ||||
|  | ||||
|                     <TextView android:id="@+id/detailThumbsDownCountView" | ||||
|                         android:layout_width="wrap_content" | ||||
|                         android:layout_height="wrap_content" | ||||
|                         android:textSize="@dimen/video_item_detail_likes_text_size" | ||||
|                         android:textAppearance="?android:attr/textAppearanceMedium" | ||||
|                         android:text="100" /> | ||||
|                 </LinearLayout> | ||||
|  | ||||
|                 <RelativeLayout | ||||
|                     android:layout_width="match_parent" | ||||
|                     android:layout_height="wrap_content" | ||||
|                     android:layout_below="@+id/linearLayout" | ||||
|                     android:id="@+id/detailUploaderWrapView" | ||||
|                     android:layout_marginTop="12dp"> | ||||
|  | ||||
|                     <View | ||||
|                         android:background="#000" | ||||
|                         android:layout_width="match_parent" | ||||
|                         android:layout_height="1px" /> | ||||
|  | ||||
|                     <de.hdodenhof.circleimageview.CircleImageView android:id="@+id/detailUploaderThumbnailView" | ||||
|                         android:contentDescription="@string/detail_uploader_thumbnail_view_description" | ||||
|                         android:layout_width="@dimen/video_item_detail_uploader_image_size" | ||||
|                         android:layout_height="@dimen/video_item_detail_uploader_image_size" | ||||
|                         android:src="@drawable/buddy" | ||||
|                         android:layout_marginLeft="10dp" | ||||
|                         android:layout_marginStart="10dp" | ||||
|                         android:layout_alignParentLeft="true" | ||||
|                         android:layout_alignParentStart="true" | ||||
|                         android:layout_marginTop="5dp" | ||||
|                         android:layout_marginBottom="5dp"/> | ||||
|  | ||||
|                     <TextView android:id="@+id/detailUploaderView" | ||||
|                         android:layout_width="wrap_content" | ||||
|                         android:layout_height="wrap_content" | ||||
|                         android:textStyle="bold" | ||||
|                         android:textSize="@dimen/video_item_detail_uploader_text_size" | ||||
|                         android:textAppearance="?android:attr/textAppearanceLarge" | ||||
|                         android:text="Uploader" | ||||
|                         android:layout_centerVertical="true" | ||||
|                         android:layout_toRightOf="@+id/detailUploaderThumbnailView" | ||||
|                         android:layout_toEndOf="@+id/detailUploaderThumbnailView" | ||||
|                         android:layout_marginLeft="15dp" | ||||
|                         android:layout_marginStart="28dp" /> | ||||
|  | ||||
|                     <View | ||||
|                         android:background="#000" | ||||
|                         android:layout_width="match_parent" | ||||
|                         android:layout_height="1px" | ||||
|                         android:layout_below="@id/detailUploaderThumbnailView"/> | ||||
|                 </RelativeLayout> | ||||
|  | ||||
|                 <RelativeLayout android:id="@+id/detailNextVideoRootLayout" | ||||
|                     android:layout_width="match_parent" | ||||
|                     android:layout_height="match_parent" | ||||
|                     android:layout_gravity="center_horizontal|bottom" | ||||
|                     android:layout_below="@+id/detailUploaderWrapView" | ||||
|                     android:layout_centerHorizontal="true" | ||||
|                     android:layout_marginTop="10dp"> | ||||
|  | ||||
|                     <TextView android:id="@+id/detailNextVideoTitle" | ||||
|                         android:layout_width="wrap_content" | ||||
|                         android:layout_height="wrap_content" | ||||
|                         android:layout_centerHorizontal="true" | ||||
|                         android:textSize="@dimen/video_item_detail_next_text_size" | ||||
|                         android:textAppearance="?android:attr/textAppearanceMedium" | ||||
|                         android:text="@string/next_video_title" | ||||
|                         android:textAllCaps="true" /> | ||||
|  | ||||
|                     <RelativeLayout android:id="@+id/detailNextVidButtonAndContentLayout" | ||||
|                         android:layout_width="match_parent" | ||||
|                         android:layout_height="match_parent" | ||||
|                         android:layout_below="@id/detailNextVideoTitle"> | ||||
|                         <FrameLayout | ||||
|                             android:id="@+id/detailNextVideoFrame" | ||||
|                             android:layout_width="match_parent" | ||||
|                             android:layout_height="wrap_content"/> | ||||
|                         <Button | ||||
|                             android:id="@+id/detailNextVideoButton" | ||||
|                             android:layout_width="match_parent" | ||||
|                             android:layout_height="match_parent" | ||||
|                             android:layout_alignTop="@id/detailNextVideoFrame" | ||||
|                             android:layout_alignBottom="@id/detailNextVideoFrame" | ||||
|                             android:background="?attr/selectableItemBackground"/> | ||||
|                     </RelativeLayout> | ||||
|                     <TextView android:id="@+id/detailSimilarTitle" | ||||
|                         android:layout_width="wrap_content" | ||||
|                         android:layout_height="wrap_content" | ||||
|                         android:layout_centerHorizontal="true" | ||||
|                         android:textSize="@dimen/video_item_detail_next_text_size" | ||||
|                         android:textAppearance="?android:attr/textAppearanceMedium" | ||||
|                         android:text="@string/similar_videos_btn_text" | ||||
|                         android:layout_below="@id/detailNextVidButtonAndContentLayout" | ||||
|                         android:textAllCaps="true" /> | ||||
|                     <LinearLayout | ||||
|                         android:id="@+id/similarVideosView" | ||||
|                         android:orientation="vertical" | ||||
|                         android:layout_width="match_parent" | ||||
|                         android:layout_height="match_parent" | ||||
|                         android:layout_below="@id/detailSimilarTitle"> | ||||
|                     </LinearLayout> | ||||
|                 </RelativeLayout> | ||||
|             </RelativeLayout> | ||||
|         </RelativeLayout> | ||||
|     </com.nirhart.parallaxscroll.views.ParallaxScrollView> | ||||
| </FrameLayout> | ||||
							
								
								
									
										125
									
								
								app/src/main/res/layout/activity_error.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,125 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     tools:context=".ErrorActivity"> | ||||
|  | ||||
|     <ScrollView | ||||
|         android:id="@+id/scrollView" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent"> | ||||
|  | ||||
|         <LinearLayout | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:orientation="vertical" | ||||
|             android:paddingBottom="@dimen/activity_vertical_margin" | ||||
|             android:paddingLeft="@dimen/activity_horizontal_margin" | ||||
|             android:paddingRight="@dimen/activity_horizontal_margin" | ||||
|             android:paddingTop="@dimen/activity_vertical_margin" | ||||
|             android:focusable="true" | ||||
|             android:focusableInTouchMode="true"> | ||||
|  | ||||
|             <TextView | ||||
|                 android:id="@+id/errorSorryView" | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:textAppearance="?android:attr/textAppearanceLarge" | ||||
|                 android:gravity="center" | ||||
|                 android:text="@string/sorry_string" | ||||
|                 android:textStyle="bold" /> | ||||
|  | ||||
|             <TextView | ||||
|                 android:id="@+id/messageWhatHappenedView" | ||||
|                 android:paddingTop="@dimen/activity_vertical_margin" | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:textAppearance="?android:attr/textAppearanceMedium" | ||||
|                 android:text="@string/what_happened_headline"/> | ||||
|  | ||||
|             <TextView | ||||
|                 android:id="@+id/errorMessageView" | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:textColor="@android:color/black" | ||||
|                 android:text="@string/info_labels"/> | ||||
|  | ||||
|             <TextView | ||||
|                 android:id="@+id/errorDeviceHeadlineView" | ||||
|                 android:paddingTop="@dimen/activity_vertical_margin" | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:textAppearance="?android:attr/textAppearanceMedium" | ||||
|                 android:text="@string/what_device_headline"/> | ||||
|              | ||||
|             <LinearLayout | ||||
|                 android:id="@+id/errorInfoLayout" | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:orientation="horizontal"> | ||||
|                  | ||||
|                 <TextView | ||||
|                     android:id="@+id/errorInfoLabelsView" | ||||
|                     android:layout_width="wrap_content" | ||||
|                     android:layout_height="wrap_content" | ||||
|                     android:textColor="@android:color/black" | ||||
|                     android:text="@string/info_labels"/> | ||||
|  | ||||
|                 <HorizontalScrollView | ||||
|                     android:paddingLeft="16dp" | ||||
|                     android:layout_width="fill_parent" | ||||
|                     android:layout_height="wrap_content"> | ||||
|  | ||||
|                     <TextView | ||||
|                         android:id="@+id/errorInfosView" | ||||
|                         android:layout_width="wrap_content" | ||||
|                         android:layout_height="wrap_content" /> | ||||
|  | ||||
|                 </HorizontalScrollView> | ||||
|  | ||||
|             </LinearLayout> | ||||
|  | ||||
|             <TextView | ||||
|                 android:id="@+id/errorDetailView" | ||||
|                 android:paddingTop="@dimen/activity_vertical_margin" | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:textAppearance="?android:attr/textAppearanceMedium" | ||||
|                 android:text="@string/error_details_headline"/> | ||||
|  | ||||
|             <HorizontalScrollView | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:id="@+id/horizontalScrollView" | ||||
|                 android:layout_gravity="center" > | ||||
|                 <TextView | ||||
|                     android:id="@+id/errorView" | ||||
|                     android:layout_width="wrap_content" | ||||
|                     android:layout_height="wrap_content" | ||||
|                     android:typeface="monospace"/> | ||||
|             </HorizontalScrollView> | ||||
|  | ||||
|             <TextView | ||||
|                 android:id="@+id/errorYourComment" | ||||
|                 android:paddingTop="@dimen/activity_vertical_margin" | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:textAppearance="?android:attr/textAppearanceMedium" | ||||
|                 android:text="@string/your_comment"/> | ||||
|  | ||||
|             <EditText | ||||
|                 android:id="@+id/errorCommentBox" | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="wrap_content"/> | ||||
|  | ||||
|             <Button | ||||
|                 android:id="@+id/errorReportButton" | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:text="@string/error_report_button_text" /> | ||||
|  | ||||
|         </LinearLayout> | ||||
|     </ScrollView> | ||||
|  | ||||
| </FrameLayout> | ||||
| @@ -2,7 +2,7 @@ | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     tools:context="org.schabi.newpipe.PlayVideoActivity" | ||||
|     tools:context=".player.PlayVideoActivity" | ||||
|     android:gravity="center"> | ||||
|  | ||||
|     <VideoView android:id="@+id/video_view" | ||||
|   | ||||
							
								
								
									
										12
									
								
								app/src/main/res/layout/against_drm_fragment.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,12 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:orientation="vertical" android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent"> | ||||
|  | ||||
|  | ||||
|  | ||||
|     <TextView | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent" /> | ||||
|  | ||||
| </LinearLayout> | ||||
							
								
								
									
										44
									
								
								app/src/main/res/layout/exo_player_activity.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,44 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <!-- Copyright (C) 2014 The Android Open Source Project | ||||
|  | ||||
|      Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|      you may not use this file except in compliance with the License. | ||||
|      You may obtain a copy of the License at | ||||
|  | ||||
|           http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|      Unless required by applicable law or agreed to in writing, software | ||||
|      distributed under the License is distributed on an "AS IS" BASIS, | ||||
|      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|      See the License for the specific language governing permissions and | ||||
|      limitations under the License. | ||||
| --> | ||||
| <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:id="@+id/root" | ||||
|     android:focusable="true" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:keepScreenOn="true"> | ||||
|  | ||||
|   <com.google.android.exoplayer.AspectRatioFrameLayout android:id="@+id/video_frame" | ||||
|       android:layout_width="match_parent" | ||||
|       android:layout_height="match_parent" | ||||
|       android:layout_gravity="center"> | ||||
|  | ||||
|     <SurfaceView android:id="@+id/surface_view" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent" | ||||
|         android:layout_gravity="center"/> | ||||
|  | ||||
|     <View android:id="@+id/shutter" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent" | ||||
|         android:background="@android:color/black"/> | ||||
|  | ||||
|     <com.google.android.exoplayer.text.SubtitleLayout android:id="@+id/subtitles" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent"/> | ||||
|  | ||||
|   </com.google.android.exoplayer.AspectRatioFrameLayout> | ||||
|  | ||||
| </FrameLayout> | ||||
| @@ -1,17 +1,21 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:id="@+id/content" | ||||
| <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:id="@+id/notificationContent" | ||||
|     android:layout_width="fill_parent" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:clickable="true" | ||||
|     android:gravity="center_vertical" | ||||
|     android:orientation="horizontal" | ||||
|     android:background="@color/background_notification_color" | ||||
|     tools:targetApi="jelly_bean"> | ||||
|     android:background="@color/background_notification_color"> | ||||
|  | ||||
|     <LinearLayout | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="64dp" | ||||
|         android:gravity="center_vertical" | ||||
|         android:orientation="horizontal"> | ||||
|  | ||||
|         <ImageView | ||||
|         android:id="@+id/backgroundCover" | ||||
|             android:id="@+id/notificationCover" | ||||
|             android:layout_width="64dp" | ||||
|             android:layout_height="64dp" | ||||
|             android:src="@drawable/dummy_thumbnail" | ||||
| @@ -25,7 +29,7 @@ | ||||
|             android:orientation="vertical" > | ||||
|  | ||||
|             <TextView | ||||
|             android:id="@+id/backgroundSongName" | ||||
|                 android:id="@+id/notificationSongName" | ||||
|                 style="@android:style/TextAppearance.StatusBar.EventContent.Title" | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
| @@ -34,7 +38,7 @@ | ||||
|                 android:text="title" /> | ||||
|  | ||||
|             <TextView | ||||
|             android:id="@+id/backgroundArtist" | ||||
|                 android:id="@+id/notificationArtist" | ||||
|                 style="@android:style/TextAppearance.StatusBar.EventContent" | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
| @@ -44,7 +48,7 @@ | ||||
|         </LinearLayout> | ||||
|  | ||||
|         <ImageButton | ||||
|         android:id="@+id/backgroundPlayPause" | ||||
|             android:id="@+id/notificationPlayPause" | ||||
|             android:layout_width="40dp" | ||||
|             android:layout_height="40dp" | ||||
|             android:layout_margin="5dp" | ||||
| @@ -54,7 +58,7 @@ | ||||
|             android:src="@drawable/ic_pause_white_24dp" /> | ||||
|  | ||||
|         <ImageButton | ||||
|         android:id="@+id/backgroundStop" | ||||
|             android:id="@+id/notificationStop" | ||||
|             android:layout_width="40dp" | ||||
|             android:layout_height="40dp" | ||||
|             android:layout_margin="5dp" | ||||
| @@ -62,5 +66,7 @@ | ||||
|             android:clickable="true" | ||||
|             android:scaleType="fitXY" | ||||
|             android:src="@drawable/ic_close_white_24dp" /> | ||||
|  | ||||
|     </LinearLayout> | ||||
|  | ||||
|  | ||||
| </RelativeLayout> | ||||
| @@ -1,15 +1,13 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:id="@+id/content" | ||||
|     android:id="@+id/notificationContent" | ||||
|     android:layout_width="fill_parent" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:clickable="true" | ||||
|     android:background="@color/background_notification_color" | ||||
|     tools:targetApi="jelly_bean" > | ||||
|     android:background="@color/background_notification_color"> | ||||
|  | ||||
|     <ImageView | ||||
|         android:id="@+id/backgroundCover" | ||||
|         android:id="@+id/notificationCover" | ||||
|         android:layout_width="128dp" | ||||
|         android:layout_height="128dp" | ||||
|         android:layout_marginRight="8dp" | ||||
| @@ -19,13 +17,13 @@ | ||||
|     <LinearLayout | ||||
|         android:layout_width="fill_parent" | ||||
|         android:layout_height="fill_parent" | ||||
|         android:layout_above="@+id/backgroundButtons" | ||||
|         android:layout_toRightOf="@+id/backgroundCover" | ||||
|         android:layout_above="@+id/notificationButtons" | ||||
|         android:layout_toRightOf="@+id/notificationCover" | ||||
|         android:gravity="center_vertical" | ||||
|         android:orientation="vertical" > | ||||
|  | ||||
|         <TextView | ||||
|             android:id="@+id/backgroundSongName" | ||||
|             android:id="@+id/notificationSongName" | ||||
|             style="@android:style/TextAppearance.StatusBar.EventContent.Title" | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
| @@ -35,7 +33,7 @@ | ||||
|             android:text="title" /> | ||||
|  | ||||
|         <TextView | ||||
|             android:id="@+id/backgroundArtist" | ||||
|             android:id="@+id/notificationArtist" | ||||
|             style="@android:style/TextAppearance.StatusBar.EventContent" | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
| @@ -45,7 +43,7 @@ | ||||
|     </LinearLayout> | ||||
|  | ||||
|     <ImageButton | ||||
|         android:id="@+id/backgroundStop" | ||||
|         android:id="@+id/notificationStop" | ||||
|         android:layout_width="30dp" | ||||
|         android:layout_height="30dp" | ||||
|         android:layout_alignParentRight="true" | ||||
| @@ -56,16 +54,16 @@ | ||||
|         android:src="@drawable/ic_close_white_24dp" /> | ||||
|  | ||||
|     <RelativeLayout | ||||
|         android:id="@+id/backgroundButtons" | ||||
|         android:id="@+id/notificationButtons" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="50dp" | ||||
|         android:layout_alignBottom="@id/backgroundCover" | ||||
|         android:layout_alignBottom="@id/notificationCover" | ||||
|         android:layout_alignParentRight="true" | ||||
|         android:layout_toRightOf="@+id/backgroundCover" | ||||
|         android:layout_toRightOf="@+id/notificationCover" | ||||
|         android:orientation="horizontal" > | ||||
|  | ||||
|         <ImageButton | ||||
|             android:id="@+id/backgroundPlayPause" | ||||
|             android:id="@+id/notificationPlayPause" | ||||
|             android:layout_width="40dp" | ||||
|             android:layout_height="40dp" | ||||
|             android:background="#00ffffff" | ||||
|   | ||||
							
								
								
									
										9
									
								
								app/src/main/res/menu/error_menu.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <menu xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto"> | ||||
|  | ||||
|     <item android:id="@+id/menu_item_share_error" | ||||
|         android:title="@string/share" | ||||
|         app:showAsAction="ifRoom" | ||||
|         android:icon="@drawable/ic_share_black"/> | ||||
| </menu> | ||||
| @@ -10,4 +10,8 @@ | ||||
|     <item android:id="@+id/action_settings" | ||||
|         app:showAsAction="never" | ||||
|         android:title="@string/settings"/> | ||||
|  | ||||
|     <item android:id="@+id/action_report_error" | ||||
|         app:showAsAction="never" | ||||
|         android:title="@string/report_error" /> | ||||
| </menu> | ||||
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 9.9 KiB | 
							
								
								
									
										117
									
								
								app/src/main/res/values-ar/strings.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,117 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <resources> | ||||
|     <string name="abc_action_bar_home_description">Navigate home</string> | ||||
|     <string name="abc_action_bar_home_description_format">%1$s, %2$s</string> | ||||
|     <string name="abc_action_bar_home_subtitle_description_format">%1$s, %2$s, %3$s</string> | ||||
|     <string name="abc_action_bar_up_description">Navigate up</string> | ||||
|     <string name="abc_action_menu_overflow_description">More options</string> | ||||
|     <string name="abc_action_mode_done">Done</string> | ||||
|     <string name="abc_activity_chooser_view_see_all">See all</string> | ||||
|     <string name="abc_activitychooserview_choose_application">Choose an app</string> | ||||
|     <string name="abc_capital_off">OFF</string> | ||||
|     <string name="abc_capital_on">ON</string> | ||||
|     <string name="abc_search_hint">Search…</string> | ||||
|     <string name="abc_searchview_description_clear">Clear query</string> | ||||
|     <string name="abc_searchview_description_query">Search query</string> | ||||
|     <string name="abc_searchview_description_search">Search</string> | ||||
|     <string name="abc_searchview_description_submit">Submit query</string> | ||||
|     <string name="abc_searchview_description_voice">Voice search</string> | ||||
|     <string name="abc_shareactionprovider_share_with">Share with</string> | ||||
|     <string name="abc_shareactionprovider_share_with_application">Share with %s</string> | ||||
|     <string name="abc_toolbar_collapse_description">Collapse</string> | ||||
|     <string name="status_bar_notification_info_overflow">999+</string> | ||||
|     <string name="autoplay_through_intent_summary">"بدء تشغيل الفيديو تلقائيًا عندما يتم فتحه من تطبيق أخر."</string> | ||||
|     <string name="autoplay_through_intent_title">التشغيل التلقائي</string> | ||||
|     <string name="background_player_name">مشغل NewPipe في الخلفية</string> | ||||
|     <string name="background_player_playing_toast">جاري التشغيل في الخلفية</string> | ||||
|     <string name="cancel">إلغاء</string> | ||||
|     <string name="choose_browser">إختر متصفح:</string> | ||||
|     <string name="dark_theme_title">مظلم</string> | ||||
|     <string name="default_audio_format_title">صيغة الصوت الإفتراضية</string> | ||||
|     <string name="default_resolution_title">الدقة الإفتراضية</string> | ||||
|     <string name="detail_dislikes_img_view_description">عدم الإعجاب</string> | ||||
|     <string name="detail_likes_img_view_description">الإعجابات</string> | ||||
|     <string name="detail_thumbnail_view_description">صور معاينة الفيديو</string> | ||||
|     <string name="detail_uploader_thumbnail_view_description">"Uploader's userpic thumbnail"</string> | ||||
|     <string name="did_you_mean">هل تقصد:</string> | ||||
|     <string name="download">تنزيل</string> | ||||
|     <string name="download_dialog_title">تنزيل</string> | ||||
|     <string name="download_path_audio_dialog_title">أدخل مسار التنزيل للملفات الصوتية.</string> | ||||
|     <string name="download_path_audio_summary">مسار حفظ التنزيلات الصوتية في.</string> | ||||
|     <string name="download_path_audio_title">مسار الصوتيات المحفوظة</string> | ||||
|     <string name="download_path_dialog_title">أدخل مسار التنزيل لملفات الفيديو</string> | ||||
|     <string name="download_path_summary">مسار حفظ تنزيلات الفيديو في.</string> | ||||
|     <string name="download_path_title">مسار الفيديوهات المحفوظة</string> | ||||
|     <string name="err_dir_create">"لا يمكن إنشاء مجلد للتنزيلات في '%1$s'"</string> | ||||
|     <string name="info_dir_created">"تم إنشاء مجلد تنزيلات في '%1$s'"</string> | ||||
|     <string name="install">تثبيت</string> | ||||
|     <string name="kore_not_found">تطبيق Kore غير موجود. هل تريد تثبيته؟</string> | ||||
|     <string name="light_theme_title">مضيء</string> | ||||
|     <string name="list_thumbnail_view_description">صور معاينة الفيديو</string> | ||||
|     <string name="loading">جاري التحميل</string> | ||||
|     <string name="m4a_description">m4a — جودة أفضل</string> | ||||
|     <string name="network_error">خطأ في الشبكة</string> | ||||
|     <string name="next_video_title">الفيديو التالي</string> | ||||
|     <string name="no_player_found">لا يوجد مشغل فيديو. هل تريد تثبيت VLC ؟</string> | ||||
|     <string name="open_in_browser">فتح في المتصفح</string> | ||||
|     <string name="play_audio">صوت</string> | ||||
|     <string name="play_btn_text">تشغيل</string> | ||||
|     <string name="play_with_kodi_title">تشغيل بواسطة Kodi</string> | ||||
|     <string name="screen_rotation">تدوير</string> | ||||
|     <string name="search">بحث</string> | ||||
|     <string name="search_language_title">لغة المحتوى المفضل</string> | ||||
|     <string name="search_page">صفحة البحث:</string> | ||||
|     <string name="settings">الإعدادات</string> | ||||
|     <string name="settings_activity_title">الإعدادات</string> | ||||
|     <string name="settings_category_appearance_title">المظهر</string> | ||||
|     <string name="settings_category_other_title">تعريب JetSub مدونة درويديات</string> | ||||
|     <string name="settings_category_video_audio_title">الفيديو والصوتيات</string> | ||||
|     <string name="share">مشاركة</string> | ||||
|     <string name="share_dialog_title">مشاركة بواسطة:</string> | ||||
|     <string name="show_next_and_similar_title">عرض التالي والفيديوهات المشابهة</string> | ||||
|     <string name="show_play_with_kodi_summary">عرض خيار لتشغيل الفيديو بواسطة Kodi Media Center.</string> | ||||
|     <string name="show_play_with_kodi_title">عرض خيار التشغيل بواسطة Kodi.</string> | ||||
|     <string name="similar_videos_btn_text">الفيديوهات المشابهة</string> | ||||
|     <string name="theme_title">الثيم</string> | ||||
|     <string name="upload_date_text">تم الرفع في %1$s</string> | ||||
|     <string name="url_not_supported_toast">الرابط غير مدعوم</string> | ||||
|     <string name="use_external_audio_player_title">استخدام مشغل صوتيات خارجي</string> | ||||
|     <string name="use_external_video_player_title">استخدام مشغل فيديو خارجي</string> | ||||
|     <string name="use_tor_summary">إجراء التنزيلات من خلال استخدام بروكسي Tor لزيادة الخصوصية ( تشغيل الفيديو المباشر غير مدعوم حتى الأن )</string> | ||||
|     <string name="use_tor_title">استخدام Tor</string> | ||||
|     <string name="view_count_text">%1$s المشاهدات</string> | ||||
|     <string name="webm_description">WebM</string> | ||||
|     <string name="blocked_by_gema">Blocked by GEMA.</string> | ||||
|     <string name="content_not_available">المحتوى غير متاح.</string> | ||||
|     <string name="could_not_load_thumbnails">لم يتمكن من تحميل كل صور المعاينة</string> | ||||
|     <string name="general_error">خطأ</string> | ||||
|     <string name="parsing_error">لا يمكن تحليل الموقع.</string> | ||||
|     <string name="youtube_signature_decryption_error">لا يمكن فك تشفير توقيع رابط الفيديو.</string> | ||||
|     <string name="app_name">NewPipe</string> | ||||
|     <string name="appbar_scrolling_view_behavior">android.support.design.widget.AppBarLayout$ScrollingViewBehavior</string> | ||||
|     <string name="autoplay_through_intent_key">autoplay_through_intent</string> | ||||
|     <string name="background_player_time_text">%1$s - NewPipe</string> | ||||
|     <string name="c3s_url">https://www.c3s.cc/</string> | ||||
|     <string name="character_counter_pattern">%1$d / %2$d</string> | ||||
|     <string name="default_audio_format_key">default_audio_format</string> | ||||
|     <string name="default_audio_format_value">m4a</string> | ||||
|     <string name="default_language_value">en</string> | ||||
|     <string name="default_resolution_key">default_resolution_preference</string> | ||||
|     <string name="default_resolution_value">360p</string> | ||||
|     <string name="default_theme_value">0</string> | ||||
|     <string name="download_path_audio_key">download_path_audio</string> | ||||
|     <string name="download_path_key">download_path</string> | ||||
|     <string name="fdroid_kore_url">https://f-droid.org/repository/browse/?fdfilter=Kore&fdid=org.xbmc.kore</string> | ||||
|     <string name="fdroid_vlc_url">https://f-droid.org/repository/browse/?fdfilter=vlc&fdid=org.videolan.vlc</string> | ||||
|     <string name="search_language_key">search_language</string> | ||||
|     <string name="settings_category_appearance">settings_category_appearance</string> | ||||
|     <string name="settings_category_other">settings_category_other</string> | ||||
|     <string name="settings_category_video_audio">settings_category_video_audio</string> | ||||
|     <string name="show_next_video_key">show_next_video</string> | ||||
|     <string name="show_play_with_kodi_key">show_play_with_kodi</string> | ||||
|     <string name="theme_key">الثيمات</string> | ||||
|     <string name="title_videoitem_detail">NewPipe</string> | ||||
|     <string name="use_external_audio_player_key">use_external_audio_player</string> | ||||
|     <string name="use_external_video_player_key">use_external_video_player</string> | ||||
|     <string name="use_tor_key">use_tor</string> | ||||
| </resources> | ||||
							
								
								
									
										83
									
								
								app/src/main/res/values-cs/strings.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,83 @@ | ||||
| <?xml version='1.0' encoding='UTF-8'?> | ||||
| <resources><string name="upload_date_text">Publikováno %1$s</string> | ||||
|     <string name="no_player_found">Žádný přehrávač nenalezen. Nainstalovat VLC?</string> | ||||
|     <string name="install">Instalovat</string> | ||||
|     <string name="cancel">Zrušit</string> | ||||
|     <string name="open_in_browser">Otevřít v prohlížeči</string> | ||||
|     <string name="share">Sdílet</string> | ||||
|     <string name="loading">Načítám</string> | ||||
|     <string name="download">Stáhnout</string> | ||||
|     <string name="search">Vyhledat</string> | ||||
|     <string name="settings">Nastavení</string> | ||||
|     <string name="did_you_mean">Měli jste na mysli: %1$s?</string> | ||||
|     <string name="search_page">"Vyhledat stránku: "</string> | ||||
|     <string name="share_dialog_title">Sdílet s:</string> | ||||
|     <string name="choose_browser">Vybrat prohlížeč:</string> | ||||
|     <string name="screen_rotation">otočení</string> | ||||
|     <string name="settings_activity_title">Nastavení</string> | ||||
|     <string name="use_external_video_player_title">Použít externí video přehrávač</string> | ||||
|     <string name="use_external_audio_player_title">Použít externí audio přehrávač</string> | ||||
|  | ||||
|     <string name="download_path_audio_summary">Cesta, kde se uloží audio po stažení.</string> | ||||
|     <string name="download_path_audio_dialog_title">Zadejte umístění pro uložení audio souborů.</string> | ||||
|  | ||||
|     <string name="download_path_audio_title">Umístění pro stažené audio</string> | ||||
|     <string name="autoplay_through_intent_title">Automatické přehrávání skrze Intent</string> | ||||
|     <string name="autoplay_through_intent_summary">Automaticky přehrávat video, jestliže je volané z jiné aplikace.</string> | ||||
|     <string name="default_resolution_title">Výchozí rozlišení</string> | ||||
|     <string name="play_with_kodi_title">Přehrát s Kodi</string> | ||||
|     <string name="kore_not_found">Aplikace Kore nenalezena. Nainstalovat Kore?</string> | ||||
|     <string name="view_count_text">%1$s zhlédnutí</string> | ||||
|     <string name="background_player_name">NewPipe Přehrávač na pozadí</string> | ||||
|     <string name="download_path_title">Umístění pro stažené video</string> | ||||
|     <string name="download_path_summary">Cesta, kde se uloží video po stažení.</string> | ||||
|     <string name="download_path_dialog_title">Zadejte umístění pro uložená videa</string> | ||||
|  | ||||
|     <string name="show_play_with_kodi_title">Zobrazit možnost \"Přehrát s Kodi\"</string> | ||||
|     <string name="show_play_with_kodi_summary">Zobrazit možnost přehrát video s multimediálním centrem Kodi.</string> | ||||
|     <string name="play_audio">Audio</string> | ||||
|     <string name="default_audio_format_title">Výchozí audio formát</string> | ||||
|     <string name="webm_description">WebM — svobodný formát</string> | ||||
|     <string name="m4a_description">m4a — lepší kvalita</string> | ||||
|     <string name="theme_title">Téma</string> | ||||
|     <string name="dark_theme_title">Tmavý</string> | ||||
|     <string name="light_theme_title">Světlý</string> | ||||
|  | ||||
|     <string name="download_dialog_title">Stažení</string> | ||||
|     <string name="next_video_title">Následující video</string> | ||||
|     <string name="show_next_and_similar_title">Zobrazit následující a související videa</string> | ||||
|     <string name="url_not_supported_toast">URL není podporováno</string> | ||||
|     <string name="similar_videos_btn_text">Související videa</string> | ||||
|     <string name="search_language_title">Preferovaný jazyk obsahu</string> | ||||
|     <string name="settings_category_video_audio_title">Video & audio</string> | ||||
|     <string name="settings_category_appearance_title">Vzhled</string> | ||||
|     <string name="settings_category_other_title">Ostatní</string> | ||||
|     <string name="background_player_playing_toast">Přehrávám na pozadí</string> | ||||
|     <string name="play_btn_text">Přehrát</string> | ||||
|     <string name="general_error">Chyba</string> | ||||
|     <string name="network_error">Chyba sítě</string> | ||||
|     <string name="could_not_load_thumbnails">Nebylo možné stáhnout všechny náhledy</string> | ||||
|     <string name="youtube_signature_decryption_error">Nebylo možné dekódovat URL videa.</string> | ||||
|     <string name="parsing_error">Nebylo možné analyzovat webovou stránku.</string> | ||||
|     <string name="content_not_available">Obsah není k dispozici.</string> | ||||
|     <string name="blocked_by_gema">Obsah blokuje GEMA.</string> | ||||
|  | ||||
|     <string name="list_thumbnail_view_description">Náhled videa</string> | ||||
|     <string name="detail_thumbnail_view_description">Náhled videa</string> | ||||
|     <string name="detail_uploader_thumbnail_view_description">Náhled uploadera</string> | ||||
|     <string name="detail_likes_img_view_description">To se mi líbí</string> | ||||
|     <string name="detail_dislikes_img_view_description">To se mi nelíbí</string> | ||||
|     <string name="use_tor_title">Použít Tor</string> | ||||
|     <string name="use_tor_summary">Vynutit stahování skrz Tor pro zvýšené soukromí (streaming není zatím podporován)</string> | ||||
|  | ||||
|     <string name="err_dir_create">Nebylo možné vytvořit složku pro stažené soubory \'%1$s\'</string> | ||||
|     <string name="info_dir_created">Vytvořena složka pro stažené soubory \'%1$s\'</string> | ||||
| <string name="autoplay_by_calling_app_title">Automaticky přehrávat při otevření z jiné aplikace.</string> | ||||
|     <string name="autoplay_by_calling_app_summary">Automaticky přehrát video, když je NewPipe otevřen z jiné aplikace.</string> | ||||
|     <string name="content">Obsah</string> | ||||
|     <string name="show_age_restricted_content_title">Zobrazovat věkově omezený obsah</string> | ||||
|     <string name="video_is_age_restricted">Toto video je věkově omezeno. Povolte věkově omezená video v nastavení.</string> | ||||
|     <string name="duration_live">živě</string> | ||||
|  | ||||
|     <string name="light_parsing_error">Nemůžu kompletně parsovat web.</string> | ||||
|     </resources> | ||||
| @@ -10,7 +10,7 @@ | ||||
|     <string name="download">Download</string> | ||||
|     <string name="search">Suchen</string> | ||||
|     <string name="settings">Einstellungen</string> | ||||
|     <string name="did_you_mean">Meintest du: </string> | ||||
|     <string name="did_you_mean">Meintest du: %1$s ?</string> | ||||
|     <string name="search_page">Suchseite: </string> | ||||
|     <string name="share_dialog_title">Teilen mit:</string> | ||||
|     <string name="choose_browser">Browser:</string> | ||||
| @@ -19,7 +19,7 @@ | ||||
|     <string name="useExternalPlayerTitle">Externen Player benutzen</string> | ||||
|     <string name="download_path_title">Downloadverzeichnis für Videos</string> | ||||
|     <string name="download_path_summary">Verzeichnis in dem heruntergeladene Videos gespeichert werden.</string> | ||||
|     <string name="download_path_dialog_title">Download-Verzeichnis für Videos eingeben</string> | ||||
|     <string name="download_path_dialog_title">Downloadverzeichnis für Videos eingeben</string> | ||||
|     <string name="autoplay_through_intent_title">Automatisches Abspielen durch Intent</string> | ||||
|     <string name="autoplay_through_intent_summary">Startet ein Video automatisch wenn es von einer anderen App aufgerufen wurde.</string> | ||||
|     <string name="default_resolution_title">Standardauflösung</string> | ||||
| @@ -55,11 +55,11 @@ | ||||
|     <string name="play_btn_text">Abspielen</string> | ||||
|  | ||||
|     <string name="use_tor_title">Benutze TOR</string> | ||||
|     <string name="use_tor_summary">Erzwinge das Herunterladen durch TOR für verbesserte Privatsphäre (Videostream noch nicht unterstützt)</string> | ||||
|     <string name="use_tor_summary">(Experimentell) Erzwinge das Herunterladen durch TOR für verbesserte Privatsphäre (Videostream noch nicht unterstützt).</string> | ||||
|     <string name="background_player_name">NewPipe Hintergrundwiedergabe</string> | ||||
|     <string name="network_error">Netzwerkfehler</string> | ||||
|  | ||||
|     <string name="download_path_audio_title">Download-Verzeichnis für Musik</string> | ||||
|     <string name="download_path_audio_title">Downloadverzeichnis für Musik</string> | ||||
|     <string name="download_path_audio_summary">Verzeichnis zum Speichern heruntergeladener Audiodateien.</string> | ||||
|     <string name="download_path_audio_dialog_title">Pfad für heruntergeladene Audiodateien eingeben.</string> | ||||
|  | ||||
| @@ -78,4 +78,51 @@ | ||||
|     <string name="content_not_available">Inhalt nicht verfügbar.</string> | ||||
|     <string name="blocked_by_gema">Durch die GEMA gesperrt.</string> | ||||
|  | ||||
|     <string name="content">Inhalt</string> | ||||
|     <string name="show_age_restricted_content_title">Altersbeschränkte Inhalte anzeigen</string> | ||||
|     <string name="video_is_age_restricted">Video ist altersbeschränkt. Schalten Sie erst altersbeschränkte Videos in den Einstellungen ein.</string> | ||||
|  | ||||
|     <string name="could_not_setup_download_menu">Konnte Downloadmenü nicht einrichten.</string> | ||||
|     <string name="live_streams_not_supported">Dies ist ein LIVESTREAM. Diese werden noch nicht unterstützt.</string> | ||||
|  | ||||
|  | ||||
|     <string name="light_parsing_error">Konnte Webseite nicht vollständig parsen.</string> | ||||
|     <string name="error_report_button_text">Fehler via Mail melden</string> | ||||
|     <string name="error_snackbar_action">MELDEN</string> | ||||
|     <string name="what_device_headline">Info:</string> | ||||
|     <string name="what_happened_headline">Dies ist passiert:</string> | ||||
|     <string name="info_labels">Was:\\nAnfrage:\\nSprache des Inhalts:\\nDienst:\\nZeit (GMT):\\nVersion:\\nOS-Version:\\nGlob. IP-Bereich:</string> | ||||
|     <string name="error_details_headline">Details:</string> | ||||
|  | ||||
|  | ||||
|     <string name="enable_background_audio">Im Hintergrund abspielen</string> | ||||
|     <string name="video">Video</string> | ||||
|     <string name="audio">Audio</string> | ||||
|     <string name="text">Text</string> | ||||
|     <string name="logging_normal">Normal</string> | ||||
|     <string name="logging_verbose">Ausführlich</string> | ||||
|     <string name="retry">Wiederholen</string> | ||||
|     <string name="off">[aus]</string> | ||||
|     <string name="error_drm_unsupported_scheme">Dieses Gerät unterstützt das erforderliche DRM-Schema nicht</string> | ||||
|     <string name="error_drm_unknown">Ein unbekannter DRM-Fehler ist aufgetreten</string> | ||||
|     <string name="error_querying_decoders">Konnte Dekodierer des Gerätes nicht abrufen</string> | ||||
|     <string xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" name="error_instantiating_decoder">Konnte Dekodierer <xliff:g id="decoder_name">%1$s</xliff:g> nicht instantiieren</string> | ||||
|     <string name="storage_permission_denied">Zugriff auf den Massenspeicher wurde verweigert</string> | ||||
|     <string name="use_exoplayer_title">Benutze ExoPlayer</string> | ||||
|     <string name="use_exoplayer_summary">Experimentell</string> | ||||
| <string name="sorry_string">Entschuldigung. Dies sollte nicht passieren.</string> | ||||
|     <string name="error_snackbar_message">Entschuldigung. Es sind einige Fehler aufgetreten.</string> | ||||
|     <string name="info_searched_lbl">Gesucht:</string> | ||||
|     <string name="info_requested_stream_lbl">Angeforderter Stream:</string> | ||||
|     <string name="your_comment">Dein Kommentar (auf englisch):</string> | ||||
|     <string name="logging">Protokollierung</string> | ||||
|     <string xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" name="error_no_decoder">Dieses Gerät stellt keinen Dekodierer für <xliff:g id="mime_type">%1$s</xliff:g> bereit</string> | ||||
|     <string xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" name="error_no_secure_decoder">Dieses Gerät stellt keinen abgesicherten Dekodierer für <xliff:g id="mime_type">%1$s</xliff:g> bereit</string> | ||||
|     <string name="could_not_get_stream">Konnte keinen Stream holen.</string> | ||||
|     <string name="error_drm_not_supported">Geschützte Inhalte werden von API-Ebenen unterhalb von 18 nicht unterstützt</string> | ||||
|     <string name="autoplay_by_calling_app_title">Bei Aufruf aus einer anderen App automatisch abspielen.</string> | ||||
|     <string name="autoplay_by_calling_app_summary">Spielt ein Video automatisch ab, wenn NewPipe von einer anderen App aufgerufen wurde.</string> | ||||
|     <string name="report_error">Einen Fehler melden</string> | ||||
|     <string name="user_report">Anwenderbericht</string> | ||||
|  | ||||
|     </resources> | ||||
|   | ||||
							
								
								
									
										75
									
								
								app/src/main/res/values-eo/strings.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,75 @@ | ||||
| <?xml version='1.0' encoding='UTF-8'?> | ||||
| <resources><string name="background_player_name">NewPipe-fonludilo</string> | ||||
|     <string name="view_count_text">%1$s vidoj</string> | ||||
|     <string name="upload_date_text">Alŝultita je %1$s</string> | ||||
|     <string name="install">Instali</string> | ||||
|     <string name="cancel">Nuligi</string> | ||||
|     <string name="open_in_browser">Malfermi per retumilo</string> | ||||
|     <string name="share">Konigi</string> | ||||
|     <string name="loading">Ŝargado</string> | ||||
|     <string name="download">Elŝuti</string> | ||||
|     <string name="search">Serĉi</string> | ||||
|     <string name="settings">Agordoj</string> | ||||
|     <string name="did_you_mean">"Ĉu vi intencis: "</string> | ||||
|     <string name="search_page">"Serĉpaĝo: "</string> | ||||
|     <string name="share_dialog_title">Konigi kun:</string> | ||||
|     <string name="choose_browser">Elekti retumilon:</string> | ||||
|     <string name="screen_rotation">turno</string> | ||||
|     <string name="settings_activity_title">Agordoj</string> | ||||
|     <string name="use_external_video_player_title">Uzi eksteran videoludilon</string> | ||||
|     <string name="use_external_audio_player_title">Uzi eksteran sonludilon</string> | ||||
|  | ||||
|     <string name="default_resolution_title">Defaŭlta distingivo</string> | ||||
|     <string name="play_with_kodi_title">Ludi per Kodi</string> | ||||
|     <string name="show_play_with_kodi_title">Montri \"Ludi per Kodi\"-opcion</string> | ||||
|     <string name="play_audio">Sono</string> | ||||
|     <string name="default_audio_format_title">Defaŭlta sondosierformo</string> | ||||
|     <string name="webm_description">WebM — libera dosierformo</string> | ||||
|     <string name="m4a_description">m4a — pli bona kvalito</string> | ||||
|     <string name="theme_title">Etoso</string> | ||||
|     <string name="dark_theme_title">Malluma</string> | ||||
|     <string name="light_theme_title">Luma</string> | ||||
|  | ||||
|     <string name="download_dialog_title">Elŝuti</string> | ||||
|     <string name="next_video_title">Sekva video</string> | ||||
|     <string name="url_not_supported_toast">Ligilo ne subtenita</string> | ||||
|     <string name="similar_videos_btn_text">Similaj videoj</string> | ||||
|     <string name="search_language_title">Preferata enhavlingvo</string> | ||||
|     <string name="settings_category_video_audio_title">Video kaj sono</string> | ||||
|     <string name="settings_category_appearance_title">Apero</string> | ||||
|     <string name="settings_category_other_title">Alia</string> | ||||
|     <string name="background_player_playing_toast">Ludado fone</string> | ||||
|     <string name="play_btn_text">Ludi</string> | ||||
|     <string name="general_error">Eraro</string> | ||||
|     <string name="network_error">Reteraro</string> | ||||
|     <string name="content_not_available">Enhavo ne estas disponebla.</string> | ||||
|     <string name="blocked_by_gema">Blokita de GEMA.</string> | ||||
|  | ||||
|     <string name="detail_likes_img_view_description">Ŝatoj</string> | ||||
|     <string name="detail_dislikes_img_view_description">Malŝatoj</string> | ||||
|     <string name="use_tor_title">Uzi la programon Tor</string> | ||||
|     <string name="autoplay_through_intent_title">Ludi aŭtomate per Intent</string> | ||||
|     <string name="no_player_found">Neniu elsendlflua ludilo trovita. Ĉu instali la aplikaĵon VLC?</string> | ||||
|     <string name="kore_not_found">La aplikaĵo Kore ne estas trovita. Ĉu instali la aplikaĵon Kore?</string> | ||||
|     <string name="show_next_and_similar_title">Montri la sekvan videon kaj similajn videojn</string> | ||||
|     <string name="could_not_load_thumbnails">Ĉiuj miniaturoj ne ŝargeblas</string> | ||||
|     <string name="youtube_signature_decryption_error">La subskribo de la ligilo de la video ne malĉifreblas.</string> | ||||
|     <string name="parsing_error">La retejo ne analizeblas.</string> | ||||
|     <string name="list_thumbnail_view_description">Miniaturo de la antaŭrigardo de la video</string> | ||||
|     <string name="detail_thumbnail_view_description">Miniaturo de la antaŭrigardo de la video</string> | ||||
|     <string name="detail_uploader_thumbnail_view_description">Miniaturo de la bildo de la alŝutinto</string> | ||||
|     <string name="err_dir_create">La elŝutujo \'%1$s\' ne kreeblas</string> | ||||
|     <string name="info_dir_created">Elŝutujo \'%1$s\' kreita</string> | ||||
| <string name="download_path_title">Elŝutujo por videoj</string> | ||||
|     <string name="download_path_audio_title">Elŝutujo por muziko</string> | ||||
|     <string name="use_tor_summary">Devigi elŝuttrafikon tra Tor por pli bona privateco (elsendfluaj videoj estas ankoraŭ ne subtenitaj)</string> | ||||
|  | ||||
|     <string name="show_play_with_kodi_summary">Montri opcion por ludi videon per la aplikaĵo Kodi.</string> | ||||
|     <string name="download_path_summary">Dosierujo por konservi elŝutitajn videojn.</string> | ||||
|     <string name="download_path_audio_summary">Dosierujo por konservi elŝutitan muzikon.</string> | ||||
|     <string name="autoplay_through_intent_summary">Ludi videon aŭtomate kiam ĝi estas vokita de alia aplikaĵo.</string> | ||||
|     <string name="download_path_dialog_title">Elektu lokon por konservi elŝutitajn videojn</string> | ||||
|  | ||||
|     <string name="download_path_audio_dialog_title">Elektu lokon por konservi elŝutitan muzikon.</string> | ||||
|  | ||||
|     </resources> | ||||
| @@ -1,8 +1,8 @@ | ||||
| <?xml version='1.0' encoding='utf-8'?> | ||||
| <?xml version='1.0' encoding='UTF-8'?> | ||||
| <resources> | ||||
|     <string name="view_count_text">%1$s visitas</string> | ||||
|     <string name="upload_date_text">Subido el %1$s</string> | ||||
|     <string name="no_player_found">No se ha encontrado ningún reproductor de vídeo. Quizás quieras instalar alguno.</string> | ||||
|     <string name="no_player_found">No se ha encontrado ningún reproductor de vídeo. Quizás quieras instalar alguno. Deseas instalar VLC?</string> | ||||
|     <string name="install">Instalarlo</string> | ||||
|     <string name="cancel">Cancelar</string> | ||||
|     <string name="open_in_browser">Abrir en el navegador</string> | ||||
| @@ -17,14 +17,14 @@ | ||||
|     <string name="screen_rotation">rotación</string> | ||||
|     <string name="settings_activity_title">Ajustes</string> | ||||
|     <string name="useExternalPlayerTitle">Usar reproductor externo</string> | ||||
|     <string name="download_path_title">Descargar en…</string> | ||||
|     <string name="download_path_title">Ruta de descarga de video</string> | ||||
|     <string name="download_path_summary">Ruta donde guardar los vídeos descargados.</string> | ||||
|     <string name="download_path_dialog_title">Localización del directorio de descargas</string> | ||||
|     <string name="autoplay_through_intent_title">Reproducción automática</string> | ||||
|     <string name="autoplay_through_intent_summary">Reproducir los vídeos automaticamente cuando se llama desde otra aplicación.</string> | ||||
|     <string name="download_path_dialog_title">Ingrese el directorio de descargas para videos</string> | ||||
|     <string name="autoplay_through_intent_title">Intentar reproducción automática</string> | ||||
|     <string name="autoplay_through_intent_summary">Reproducir vídeos automáticamente cuando se llamen desde otra aplicación.</string> | ||||
|     <string name="default_resolution_title">Resolución por defecto</string> | ||||
|     <string name="play_with_kodi_title">Reproducir con Kodi</string> | ||||
|     <string name="kore_not_found">Aplicación Kore no encontrada. Kore es necesario para reproducir vídeos con Kodi media center.</string> | ||||
|     <string name="kore_not_found">Aplicación Kore no encontrada. Kore es necesario para reproducir vídeos con Kodi media center. Instalar Kore?</string> | ||||
|     <string name="installeKore">Instalar Kore</string> | ||||
|     <string name="show_play_with_kodi_title">Mostrar la opción \"Reproducir con Kodi\"</string> | ||||
|     <string name="show_play_with_kodi_summary">Muestra una opción para reproducir el vídeo con Kodi media center.</string> | ||||
| @@ -34,8 +34,51 @@ | ||||
|     <string name="m4a_description">m4a — mejor calidad</string> | ||||
|     <string name="download_dialog_title">Descargar</string> | ||||
|     <string name="next_video_title">Siguiente vídeo</string> | ||||
|     <string name="url_not_supported_toast">URL no soportada.</string> | ||||
|     <string name="url_not_supported_toast">URL no soportada</string> | ||||
|     <string name="similar_videos_btn_text">Vídeos similares</string> | ||||
| <string name="background_player_name">Reproductor de fondo NewPipe</string> | ||||
|     <string name="loading">Cargando</string> | ||||
|     <string name="use_external_video_player_title">Usar un reproductor de vídeo externo</string> | ||||
|     <string name="use_external_audio_player_title">Usar un reproductor de audio externo</string> | ||||
|  | ||||
|     <string name="theme_title">Tema</string> | ||||
|     <string name="dark_theme_title">Oscuro</string> | ||||
|     <string name="light_theme_title">Claro</string> | ||||
|  | ||||
|     <string name="settings_category_appearance_title">Apariencia</string> | ||||
|     <string name="settings_category_other_title">Otro</string> | ||||
|     <string name="background_player_playing_toast">Reproduciendo en Segundo plano</string> | ||||
|     <string name="content_not_available">Contenido no disponible.</string> | ||||
|     <string name="use_tor_title">Usar Tor</string> | ||||
|     <string name="use_tor_summary">Forzar la descarga a través de Tor para una mayor privacidad (transmisión de videos aún no es compatible)</string> | ||||
|  | ||||
|     <string name="err_dir_create">No se puede crear la carpeta de descarga \'%1$s\'</string> | ||||
|     <string name="info_dir_created">Capeta de descarga creada \'%1$s\'</string> | ||||
| <string name="download_path_audio_summary">Ruta para almacenar el audio descargado.</string> | ||||
|     <string name="download_path_audio_dialog_title">Ingrese la ruta de descarga para los archivos de audio.</string> | ||||
|  | ||||
|     <string name="blocked_by_gema">Bloqueado por GEMA.</string> | ||||
|  | ||||
|     <string name="download_path_audio_title">Ruta de descarga de audio</string> | ||||
|     <string name="settings_category_video_audio_title">Video & Audio</string> | ||||
|     <string name="play_btn_text">Reproducir</string> | ||||
|     <string name="general_error">Error</string> | ||||
|     <string name="network_error">Error de Conexión</string> | ||||
|     <string name="could_not_load_thumbnails">No se pudo cargar las miniaturas</string> | ||||
|     <string name="youtube_signature_decryption_error">No se pudo descifrar la url del video.</string> | ||||
|     <string name="parsing_error">No se pudo analizar el sitio web.</string> | ||||
|     <string name="show_next_and_similar_title">Mostrar videos similares</string> | ||||
|     <string name="search_language_title">Idioma del contenido</string> | ||||
|     <string name="list_thumbnail_view_description">Vista previa del video</string> | ||||
|     <string name="detail_thumbnail_view_description">Vista previa del video</string> | ||||
|     <string name="detail_likes_img_view_description">Me gusta</string> | ||||
|     <string name="detail_dislikes_img_view_description">No me gusta</string> | ||||
|     <string name="detail_uploader_thumbnail_view_description">Foto miniatura del usuario</string> | ||||
|     <string name="live_streams_not_supported">Esta es una transmisión en Vivo. Todavía no es compatible.</string> | ||||
|  | ||||
|  | ||||
|     <string name="content">Contenido</string> | ||||
|     <string name="show_age_restricted_content_title">Mostrar contenido con restricción de edad</string> | ||||
|     <string name="video_is_age_restricted">El video tiene restricción de edad.Habilite los videos con restricción de edad en configuración.</string> | ||||
|  | ||||
|     </resources> | ||||
|   | ||||
| @@ -1,42 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <resources> | ||||
|     <string name="view_count_text">%1$s نماها</string> | ||||
|     <string name="upload_date_text">بارگذاریشده در: %1$s</string> | ||||
|     <string name="no_player_found">هیچ پخشکنندهی جریانی یافت نشد. ممکن است بخواهید یکی نصب کنید.</string> | ||||
|     <string name="install">نصب کنید</string> | ||||
|     <string name="cancel">انصراف</string> | ||||
|     <string name="open_in_browser">بازکردن در مرورگر</string> | ||||
|     <string name="share">همرسانی</string> | ||||
|     <string name="download">بارگیری</string> | ||||
|     <string name="search">جستجو</string> | ||||
|     <string name="settings">تنظیمات</string> | ||||
|     <string name="did_you_mean">منظورتان این است: </string> | ||||
|     <string name="search_page">صفحهی جستجو: </string> | ||||
|     <string name="share_dialog_title">همرسانی با:</string> | ||||
|     <string name="choose_browser">مرورگر را برگزینید:</string> | ||||
|     <string name="screen_rotation">چرخش</string> | ||||
|     <string name="settings_activity_title">تنظیمات</string> | ||||
|     <string name="useExternalPlayerTitle">استفاده از پخشکنندهی خارجی</string> | ||||
|     <string name="download_path_title">محل بارگیری</string> | ||||
|     <string name="download_path_summary">مسیری که ویدئوهای دریافت شده در آن ذخیره میشوند.</string> | ||||
|     <string name="download_path_dialog_title">مسیر دریافت را وارد کنید</string> | ||||
|     <string name="autoplay_through_intent_title">پخش خودکار از Intent</string> | ||||
|     <string name="autoplay_through_intent_summary">ویدئو هنگامی که از برنامهی دیگری فراخوانده شد خودکار پخش میشود.</string> | ||||
|     <string name="default_resolution_title">وضوح پیشفرض</string> | ||||
|     <string name="play_with_kodi_title">پخش با Kodi</string> | ||||
|     <string name="kore_not_found">برنامهی Kore نصب نیست. برای پخش کردن ویدئوها با مرکز رسانهی Kodi، به Kore نیاز دارید.</string> | ||||
|     <string name="installeKore">نصب Kore</string> | ||||
|     <string name="show_play_with_kodi_title">نمایش گزینهی «پخش با Kodi»</string> | ||||
|     <string name="show_play_with_kodi_summary">گزینهای برای پخش کردن ویدئو با مرکز رسانهی Kodi نشان میدهد.</string> | ||||
|     <string name="play_audio">صدا</string> | ||||
|     <string name="default_audio_format_title">قالب پیشفرض صدا</string> | ||||
|     <string name="webm_description">WebM — قالبی آزاد</string> | ||||
|     <string name="m4a_description">m4a — کیفیت بهتر</string> | ||||
|     <string name="download_dialog_title">دریافت</string> | ||||
|     <string-array name="downloadOptions"> | ||||
|         <item>ویدئو</item> | ||||
|         <item>صدا</item> | ||||
|     </string-array> | ||||
|     <string name="next_video_title">ویدئوی بعدی</string> | ||||
|     <string name="url_not_supported_toast">پیوند پشتیبانی نمیشود.</string> | ||||
| </resources> | ||||
| @@ -1,4 +1,4 @@ | ||||
| <?xml version='1.0' encoding='utf-8'?> | ||||
| <?xml version='1.0' encoding='UTF-8'?> | ||||
| <resources> | ||||
|     <string name="autoplay_through_intent_summary">Lire automatiquement une vidéo lorsqu’elle a été appelée depuis une autre application.</string> | ||||
|     <string name="cancel">Annuler</string> | ||||
| @@ -68,4 +68,9 @@ | ||||
|  | ||||
|     <string name="err_dir_create">Impossible de créer le répertoire de téléchargement « %1$s »</string> | ||||
|     <string name="info_dir_created">Répertoire de téléchargement « %1$s » créé</string> | ||||
| <string name="general_error">Erreur</string> | ||||
|     <string name="parsing_error">Impossible de parser ce site web.</string> | ||||
|     <string name="content_not_available">Contenu non disponible.</string> | ||||
|     <string name="blocked_by_gema">Bloqué par GEMA.</string> | ||||
|  | ||||
|     </resources> | ||||
|   | ||||
| @@ -1,10 +0,0 @@ | ||||
| <?xml version='1.0' encoding='utf-8'?> | ||||
| <resources><string name="view_count_text">%1$s צפיות</string> | ||||
|     <string name="upload_date_text">הועלה בתאריך %1$s</string> | ||||
|     <string name="share">שתף</string> | ||||
|     <string name="search">חפש</string> | ||||
|     <string name="next_video_title">הבא</string> | ||||
|     <string name="download">הורדה</string> | ||||
|     <string name="settings">הגדרות</string> | ||||
|     <string name="settings_activity_title">הגדרות</string> | ||||
|     </resources> | ||||
| @@ -1,4 +1,4 @@ | ||||
| <?xml version='1.0' encoding='utf-8'?> | ||||
| <?xml version='1.0' encoding='UTF-8'?> | ||||
| <resources> | ||||
|     <string name="view_count_text">%1$s megtekintés</string> | ||||
|     <string name="upload_date_text">Feltöltve: %1$s</string> | ||||
| @@ -27,7 +27,7 @@ | ||||
|     <string name="kore_not_found">A Kore alkalmazás nem található. Feltelepíti a Kore lejátszót?</string> | ||||
|     <string name="installeKore">Kore telepítése</string> | ||||
|     <string name="show_play_with_kodi_title">\"Lejátszás Kodi-val\" opció mutatása</string> | ||||
|     <string name="show_play_with_kodi_summary">Mutat egy opciót a videók Kodi médiaközponttal való lejátszására</string> | ||||
|     <string name="show_play_with_kodi_summary">Mutat egy opciót a videók Kodi médiaközponttal való lejátszására.</string> | ||||
|     <string name="play_audio">Hang</string> | ||||
|     <string name="default_audio_format_title">Alapértelmezett hang formátum</string> | ||||
|     <string name="webm_description">WebM — szabad formátum</string> | ||||
| @@ -72,4 +72,10 @@ | ||||
|     <string name="detail_uploader_thumbnail_view_description">Fetöltő profilképe</string> | ||||
|     <string name="err_dir_create">Nem lehet létrehozni a letöltés mappát \'%1$s\'</string> | ||||
|     <string name="info_dir_created">Letöltés mappa létrehozása \'%1$s\'</string> | ||||
| <string name="content">Tartalom</string> | ||||
|     <string name="show_age_restricted_content_title">Mutassa korhatáros tartalmat is</string> | ||||
|     <string name="general_error">Hiba</string> | ||||
|     <string name="content_not_available">A tartalom nem elérhetö.</string> | ||||
|     <string name="blocked_by_gema">A GEMA által blokkolva.</string> | ||||
|     <string name="live_streams_not_supported">Ez egy élö közvetités. Még nem támogatva.</string> | ||||
|     </resources> | ||||
|   | ||||
| @@ -1 +0,0 @@ | ||||
| values-in | ||||
							
								
								
									
										2
									
								
								app/src/main/res/values-id/strings.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <resources></resources> | ||||
| @@ -1,3 +0,0 @@ | ||||
| <?xml version='1.0' encoding='utf-8'?> | ||||
| <resources> | ||||
| </resources> | ||||
| @@ -1,9 +1,9 @@ | ||||
| <?xml version='1.0' encoding='utf-8'?> | ||||
| <?xml version='1.0' encoding='UTF-8'?> | ||||
| <resources><string name="view_count_text">%1$s visite</string> | ||||
|     <string name="upload_date_text">Caricato in %1$s</string> | ||||
|     <string name="no_player_found">Nessun riproduttore trovato. Dovresti installarne uno.</string> | ||||
|     <string name="upload_date_text">Pubblicato il %1$s</string> | ||||
|     <string name="no_player_found">Nessun riproduttore trovato. Vuoi installare VLC?</string> | ||||
|     <string name="install">Installa</string> | ||||
|     <string name="cancel">Cancella</string> | ||||
|     <string name="cancel">Annulla</string> | ||||
|     <string name="open_in_browser">Apri nel browser</string> | ||||
|     <string name="share">Condividi</string> | ||||
|     <string name="download">Scarica</string> | ||||
| @@ -16,34 +16,74 @@ | ||||
|     <string name="screen_rotation">rotazione</string> | ||||
|     <string name="settings_activity_title">Impostazioni</string> | ||||
|     <string name="useExternalPlayerTitle">Usa un riproduttore video esterno</string> | ||||
|     <string name="download_path_title">Cartella di download</string> | ||||
|     <string name="download_path_title">Cartella dei download</string> | ||||
|     <string name="download_path_summary">Percorso dove memorizzare i video scaricati.</string> | ||||
|     <string name="download_path_dialog_title">Inserisci il percorso di download</string> | ||||
|     <string name="download_path_dialog_title">Inserisci il percorso per i download</string> | ||||
|     <string name="autoplay_through_intent_title">Auto riproduzione attraverso internet</string> | ||||
|     <string name="autoplay_through_intent_summary">Avvia automaticamente un video quando è stato chiamato da un\'altra applicazione.</string> | ||||
|     <string name="default_resolution_title">Risoluzione predefinita</string> | ||||
|     <string name="play_with_kodi_title">Riproduci con Kodi</string> | ||||
|     <string name="kore_not_found">Kore app non trovata. Kore è richiesto per riprodurre video con Kodi media center.</string> | ||||
|     <string name="kore_not_found">L\'applicazione Kore non è stata trovata. Kore è necessario per riprodurre video con Kodi media center. Vorresti installarlo?</string> | ||||
|     <string name="installeKore">Installa Kore</string> | ||||
|     <string name="show_play_with_kodi_title">Mostra l\'opzione \"Riproduci con Kodi\"</string> | ||||
|     <string name="show_play_with_kodi_summary">Mostra un opzione per riprodurre un video attraverso Kodi media center.</string> | ||||
|     <string name="play_audio">Audio</string> | ||||
|     <string name="default_audio_format_title">Formato audio predefinito</string> | ||||
|     <string name="webm_description">WedM — formato libero</string> | ||||
|     <string name="m4a_description">m4a — qualità migliore</string> | ||||
|     <string name="webm_description">WebM — formato libero</string> | ||||
|     <string name="m4a_description">m4a — qualità migliore</string> | ||||
|     <string name="download_dialog_title">Scarica</string> | ||||
|     <string name="next_video_title">Prossimo video</string> | ||||
|     <string name="show_next_and_similar_title">Mostra i video successivi e simili</string> | ||||
|     <string name="url_not_supported_toast">URL non supportato.</string> | ||||
|     <string name="show_next_and_similar_title">Mostra video a seguire e video simili</string> | ||||
|     <string name="url_not_supported_toast">URL non supportato</string> | ||||
|     <string name="similar_videos_btn_text">Video simili</string> | ||||
|     <string name="search_language_title">Lingua preferita dei contenuti</string> | ||||
|     <string name="settings_category_video_audio_title">VIDEO & AUDIO</string> | ||||
|     <string name="search_language_title">Lingua preferita per i contenuti</string> | ||||
|     <string name="settings_category_video_audio_title">Video e Audio</string> | ||||
|     <string name="settingsCategoryVideoInfoTittle">INFO</string> | ||||
|     <string name="settingsCategoryEtcTitle">ETC</string> | ||||
|  | ||||
|     <string name="list_thumbnail_view_description">Anteprima video</string> | ||||
|     <string name="detail_thumbnail_view_description">Anteprima video</string> | ||||
|     <string name="detail_uploader_thumbnail_view_description">Miniatura caricata</string> | ||||
|     <string name="detail_uploader_thumbnail_view_description">Miniatura dell\'immagine di profilo degli utenti</string> | ||||
|     <string name="detail_dislikes_img_view_description">Non mi piace</string> | ||||
|     <string name="detail_likes_img_view_description">Mi piace</string> | ||||
| <string name="err_dir_create">Impossibile creare la cartella di download \'%1$s\'</string> | ||||
|     <string name="info_dir_created">Creata la cartella per i download \'%1$s\'</string> | ||||
| <string name="background_player_name">Player in background di NewPipe</string> | ||||
|     <string name="loading">Caricamento</string> | ||||
|     <string name="use_external_video_player_title">Usa un lettore video esterno</string> | ||||
|     <string name="use_external_audio_player_title">Usa un lettore audio esterno</string> | ||||
|  | ||||
|     <string name="download_path_audio_title">Cartella dei download degli audio</string> | ||||
|     <string name="download_path_audio_summary">Cartella dove salvare gli audio scaricati.</string> | ||||
|     <string name="download_path_audio_dialog_title">Inserisci la cartella per i file audio.</string> | ||||
|  | ||||
|     <string name="theme_title">Tema</string> | ||||
|     <string name="dark_theme_title">Scuro</string> | ||||
|     <string name="light_theme_title">Chiaro</string> | ||||
|  | ||||
|     <string name="settings_category_appearance_title">Aspetto</string> | ||||
|     <string name="settings_category_other_title">Altro</string> | ||||
|     <string name="background_player_playing_toast">In riproduzione in background</string> | ||||
|     <string name="play_btn_text">Riproduci</string> | ||||
|  | ||||
|     <string name="general_error">Errore</string> | ||||
|     <string name="network_error">Errore di rete</string> | ||||
|     <string name="could_not_load_thumbnails">Impossibile caricare tutte le miniature</string> | ||||
|     <string name="youtube_signature_decryption_error">Impossibile decriptare la firma dell\'URL del video.</string> | ||||
|     <string name="content_not_available">Contenuto non disponibile.</string> | ||||
|     <string name="blocked_by_gema">Bloccato dalla GEMA.</string> | ||||
|     <string name="use_tor_title">Usa Tor</string> | ||||
|     <string name="use_tor_summary">Forza il traffico in download tramite Tor per una maggiore privacy (lo streaming dei video non è ancora supportato).</string> | ||||
|  | ||||
|     <string name="parsing_error">Impossibile analizzare il sito web.</string> | ||||
|     <string name="could_not_setup_download_menu">Impossibile impostare il menù di download.</string> | ||||
|  | ||||
|  | ||||
|     <string name="live_streams_not_supported">Questo è uno stream dal vivo. Gli stream dal vivo non sono ancora supportati.</string> | ||||
|  | ||||
|  | ||||
|     <string name="content">Contenuti</string> | ||||
|     <string name="show_age_restricted_content_title">Mostra contenuti vincolati all\'età</string> | ||||
|     <string name="video_is_age_restricted">Questo video è vincolato alla maggiore età. Per accedervi, abilita \"Mostra video vincolati all\'età\" nelle impostazioni.</string> | ||||
|  | ||||
|     </resources> | ||||
|   | ||||
 Hayden
					Hayden