mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2024-12-23 00:20:32 +00:00
Merge pull request #1 from theScrabi/master
merge from theScrabi/NewPipe
This commit is contained in:
commit
5d28b2400f
16
app/src/main/java/org/schabi/newpipe/AbstractVideoInfo.java
Normal file
16
app/src/main/java/org/schabi/newpipe/AbstractVideoInfo.java
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
|
||||||
|
/**Common properties between VideoInfo and VideoPreviewInfo.*/
|
||||||
|
public abstract class AbstractVideoInfo {
|
||||||
|
public String id = "";
|
||||||
|
public String title = "";
|
||||||
|
public String uploader = "";
|
||||||
|
//public int duration = -1;
|
||||||
|
public String thumbnail_url = "";
|
||||||
|
public Bitmap thumbnail = null;
|
||||||
|
public String webpage_url = "";
|
||||||
|
public String upload_date = "";
|
||||||
|
public long view_count = -1;
|
||||||
|
}
|
@ -48,6 +48,7 @@ public class ActionBarHandler {
|
|||||||
private String videoTitle = "";
|
private String videoTitle = "";
|
||||||
|
|
||||||
SharedPreferences defaultPreferences = null;
|
SharedPreferences defaultPreferences = null;
|
||||||
|
private int startPosition;
|
||||||
|
|
||||||
class FormatItemSelectListener implements ActionBar.OnNavigationListener {
|
class FormatItemSelectListener implements ActionBar.OnNavigationListener {
|
||||||
@Override
|
@Override
|
||||||
@ -216,12 +217,18 @@ public class ActionBarHandler {
|
|||||||
intent.putExtra(PlayVideoActivity.VIDEO_TITLE, videoTitle);
|
intent.putExtra(PlayVideoActivity.VIDEO_TITLE, videoTitle);
|
||||||
intent.putExtra(PlayVideoActivity.STREAM_URL, videoStreams[selectedStream].url);
|
intent.putExtra(PlayVideoActivity.STREAM_URL, videoStreams[selectedStream].url);
|
||||||
intent.putExtra(PlayVideoActivity.VIDEO_URL, websiteUrl);
|
intent.putExtra(PlayVideoActivity.VIDEO_URL, websiteUrl);
|
||||||
activity.startActivity(intent);
|
intent.putExtra(PlayVideoActivity.START_POSITION, startPosition);
|
||||||
|
activity.startActivity(intent); //also HERE !!!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// --------------------------------------------
|
// --------------------------------------------
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setStartPosition(int startPositionSeconds)
|
||||||
|
{
|
||||||
|
this.startPosition = startPositionSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
public void downloadVideo() {
|
public void downloadVideo() {
|
||||||
if(!videoTitle.isEmpty()) {
|
if(!videoTitle.isEmpty()) {
|
||||||
String videoSuffix = "." + MediaFormat.getSuffixById(videoStreams[selectedStream].format);
|
String videoSuffix = "." + MediaFormat.getSuffixById(videoStreams[selectedStream].format);
|
||||||
|
@ -31,6 +31,11 @@ public class Downloader {
|
|||||||
|
|
||||||
private static final String USER_AGENT = "Mozilla/5.0";
|
private static final String USER_AGENT = "Mozilla/5.0";
|
||||||
|
|
||||||
|
/**Download the text file at the supplied URL as in download(String),
|
||||||
|
* but set the HTTP header field "Accept-Language" to the supplied string.
|
||||||
|
* @param siteUrl the URL of the text file to return the contents of
|
||||||
|
* @param language the language (usually a 2-character code) to set as the preferred language
|
||||||
|
* @return the contents of the specified text file*/
|
||||||
public static String download(String siteUrl, String language) {
|
public static String download(String siteUrl, String language) {
|
||||||
String ret = "";
|
String ret = "";
|
||||||
try {
|
try {
|
||||||
@ -44,7 +49,7 @@ public class Downloader {
|
|||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
/**Common functionality between download(String url) and download(String url, String language)*/
|
||||||
private static String dl(HttpURLConnection con) {
|
private static String dl(HttpURLConnection con) {
|
||||||
StringBuffer response = new StringBuffer();
|
StringBuffer response = new StringBuffer();
|
||||||
|
|
||||||
@ -72,7 +77,10 @@ public class Downloader {
|
|||||||
return response.toString();
|
return response.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**Download (via HTTP) the text file located at the supplied URL, and return its contents.
|
||||||
|
* Primarily intended for downloading web pages.
|
||||||
|
* @param siteUrl the URL of the text file to download
|
||||||
|
* @return the contents of the specified text file*/
|
||||||
public static String download(String siteUrl) {
|
public static String download(String siteUrl) {
|
||||||
String ret = "";
|
String ret = "";
|
||||||
|
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
package org.schabi.newpipe;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by Christian Schabesberger on 10.08.15.
|
|
||||||
*
|
|
||||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
|
||||||
* Extractor.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 Extractor {
|
|
||||||
VideoInfo getVideoInfo(String siteUrl);
|
|
||||||
String getVideoUrl(String videoId);
|
|
||||||
String getVideoId(String videoUrl);
|
|
||||||
}
|
|
@ -21,6 +21,8 @@ package org.schabi.newpipe;
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**Static data about various media formats support by Newpipe, eg mime type, extension*/
|
||||||
public enum MediaFormat {
|
public enum MediaFormat {
|
||||||
// id name suffix mime type
|
// id name suffix mime type
|
||||||
MPEG_4 (0x0, "MPEG-4", "mp4", "video/mp4"),
|
MPEG_4 (0x0, "MPEG-4", "mp4", "video/mp4"),
|
||||||
@ -41,6 +43,10 @@ public enum MediaFormat {
|
|||||||
this.mimeType = mimeType;
|
this.mimeType = mimeType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**Return the friendly name of the media format with the supplied id
|
||||||
|
* @param ident the id of the media format. Currently an arbitrary, NewPipe-specific number.
|
||||||
|
* @return the friendly name of the MediaFormat associated with this ids,
|
||||||
|
* or an empty String if none match it.*/
|
||||||
public static String getNameById(int ident) {
|
public static String getNameById(int ident) {
|
||||||
for (MediaFormat vf : MediaFormat.values()) {
|
for (MediaFormat vf : MediaFormat.values()) {
|
||||||
if(vf.id == ident) return vf.name;
|
if(vf.id == ident) return vf.name;
|
||||||
@ -48,6 +54,10 @@ public enum MediaFormat {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**Return the file extension of the media format with the supplied id
|
||||||
|
* @param ident the id of the media format. Currently an arbitrary, NewPipe-specific number.
|
||||||
|
* @return the file extension of the MediaFormat associated with this ids,
|
||||||
|
* or an empty String if none match it.*/
|
||||||
public static String getSuffixById(int ident) {
|
public static String getSuffixById(int ident) {
|
||||||
for (MediaFormat vf : MediaFormat.values()) {
|
for (MediaFormat vf : MediaFormat.values()) {
|
||||||
if(vf.id == ident) return vf.suffix;
|
if(vf.id == ident) return vf.suffix;
|
||||||
@ -55,6 +65,10 @@ public enum MediaFormat {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**Return the MIME type of the media format with the supplied id
|
||||||
|
* @param ident the id of the media format. Currently an arbitrary, NewPipe-specific number.
|
||||||
|
* @return the MIME type of the MediaFormat associated with this ids,
|
||||||
|
* or an empty String if none match it.*/
|
||||||
public static String getMimeById(int ident) {
|
public static String getMimeById(int ident) {
|
||||||
for (MediaFormat vf : MediaFormat.values()) {
|
for (MediaFormat vf : MediaFormat.values()) {
|
||||||
if(vf.id == ident) return vf.mimeType;
|
if(vf.id == ident) return vf.mimeType;
|
||||||
|
@ -15,6 +15,7 @@ import android.support.v7.app.AppCompatActivity;
|
|||||||
import android.util.DisplayMetrics;
|
import android.util.DisplayMetrics;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.Display;
|
import android.view.Display;
|
||||||
|
import android.view.KeyEvent;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
@ -52,6 +53,7 @@ public class PlayVideoActivity extends AppCompatActivity {
|
|||||||
public static final String STREAM_URL = "stream_url";
|
public static final String STREAM_URL = "stream_url";
|
||||||
public static final String VIDEO_TITLE = "video_title";
|
public static final String VIDEO_TITLE = "video_title";
|
||||||
private static final String POSITION = "position";
|
private static final String POSITION = "position";
|
||||||
|
public static final String START_POSITION = "start_position";
|
||||||
|
|
||||||
private static final long HIDING_DELAY = 3000;
|
private static final long HIDING_DELAY = 3000;
|
||||||
private static final long TAB_HIDING_DELAY = 100;
|
private static final long TAB_HIDING_DELAY = 100;
|
||||||
@ -85,9 +87,34 @@ public class PlayVideoActivity extends AppCompatActivity {
|
|||||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||||
Intent intent = getIntent();
|
Intent intent = getIntent();
|
||||||
if(mediaController == null) {
|
if(mediaController == null) {
|
||||||
mediaController = new MediaController(this);
|
//prevents back button hiding media controller controls (after showing them)
|
||||||
|
//instead of exiting video
|
||||||
|
//see http://stackoverflow.com/questions/6051825
|
||||||
|
//also solves https://github.com/theScrabi/NewPipe/issues/99
|
||||||
|
mediaController = new MediaController(this) {
|
||||||
|
@Override
|
||||||
|
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||||
|
int keyCode = event.getKeyCode();
|
||||||
|
final boolean uniqueDown = event.getRepeatCount() == 0
|
||||||
|
&& event.getAction() == KeyEvent.ACTION_DOWN;
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||||
|
if (uniqueDown)
|
||||||
|
{
|
||||||
|
if (isShowing()) {
|
||||||
|
finish();
|
||||||
|
} else {
|
||||||
|
hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.dispatchKeyEvent(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
position = intent.getIntExtra(START_POSITION, 0)*1000;//convert from seconds to milliseconds
|
||||||
|
|
||||||
videoView = (VideoView) findViewById(R.id.video_view);
|
videoView = (VideoView) findViewById(R.id.video_view);
|
||||||
progressBar = (ProgressBar) findViewById(R.id.play_video_progress_bar);
|
progressBar = (ProgressBar) findViewById(R.id.play_video_progress_bar);
|
||||||
try {
|
try {
|
||||||
@ -145,11 +172,6 @@ public class PlayVideoActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostCreate(Bundle savedInstanceState) {
|
|
||||||
super.onPostCreate(savedInstanceState);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCreatePanelMenu(int featured, Menu menu) {
|
public boolean onCreatePanelMenu(int featured, Menu menu) {
|
||||||
super.onCreatePanelMenu(featured, menu);
|
super.onCreatePanelMenu(featured, menu);
|
||||||
@ -159,11 +181,6 @@ public class PlayVideoActivity extends AppCompatActivity {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPause() {
|
public void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
package org.schabi.newpipe;
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.List;
|
||||||
import java.util.Vector;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Christian Schabesberger on 26.08.15.
|
* Created by Christian Schabesberger on 26.08.15.
|
||||||
@ -27,53 +25,77 @@ import java.util.Vector;
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**Info object for opened videos, ie the video ready to play.*/
|
/**Info object for opened videos, ie the video ready to play.*/
|
||||||
public class VideoInfo {
|
public class VideoInfo extends AbstractVideoInfo {
|
||||||
public String id = "";
|
private static final String TAG = VideoInfo.class.toString();
|
||||||
public String title = "";
|
|
||||||
public String uploader = "";
|
|
||||||
public String thumbnail_url = "";
|
|
||||||
public Bitmap thumbnail = null;
|
|
||||||
public String webpage_url = "";
|
|
||||||
public String upload_date = "";
|
|
||||||
public long view_count = 0;
|
|
||||||
|
|
||||||
public String uploader_thumbnail_url = "";
|
public String uploader_thumbnail_url = "";
|
||||||
public Bitmap uploader_thumbnail = null;
|
public Bitmap uploader_thumbnail = null;
|
||||||
public String description = "";
|
public String description = "";
|
||||||
public int duration = -1;
|
|
||||||
public int age_limit = 0;
|
|
||||||
public int like_count = 0;
|
|
||||||
public int dislike_count = 0;
|
|
||||||
public String average_rating = "";
|
|
||||||
public VideoStream[] videoStreams = null;
|
public VideoStream[] videoStreams = null;
|
||||||
public AudioStream[] audioStreams = null;
|
public AudioStream[] audioStreams = null;
|
||||||
public VideoInfoItem nextVideo = null;
|
|
||||||
public VideoInfoItem[] relatedVideos = null;
|
|
||||||
public int videoAvailableStatus = VIDEO_AVAILABLE;
|
public int videoAvailableStatus = VIDEO_AVAILABLE;
|
||||||
|
public int duration = -1;
|
||||||
|
|
||||||
private static final String TAG = VideoInfo.class.toString();
|
/*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 nextVideo = null;
|
||||||
|
public List<VideoPreviewInfo> relatedVideos = null;
|
||||||
|
public int startPosition = -1;//in seconds. some metadata is not passed using a VideoInfo object!
|
||||||
|
|
||||||
public static final int VIDEO_AVAILABLE = 0x00;
|
public static final int VIDEO_AVAILABLE = 0x00;
|
||||||
public static final int VIDEO_UNAVAILABLE = 0x01;
|
public static final int VIDEO_UNAVAILABLE = 0x01;
|
||||||
public static final int VIDEO_UNAVAILABLE_GEMA = 0x02;//German DRM organisation
|
public static final int VIDEO_UNAVAILABLE_GEMA = 0x02;//German DRM organisation
|
||||||
|
|
||||||
public static class VideoStream {
|
|
||||||
public VideoStream(String url, int format, String res) {
|
public VideoInfo() {}
|
||||||
this.url = url; this.format = format; resolution = res;
|
|
||||||
|
|
||||||
|
/**Creates a new VideoInfo object from an existing AbstractVideoInfo.
|
||||||
|
* All the shared properties are copied to the new VideoInfo.*/
|
||||||
|
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 {
|
||||||
public String url = ""; //url of the stream
|
public String url = ""; //url of the stream
|
||||||
public int format = -1;
|
public int format = -1;
|
||||||
public String resolution = "";
|
public String resolution = "";
|
||||||
|
|
||||||
|
public VideoStream(String url, int format, String res) {
|
||||||
|
this.url = url; this.format = format; resolution = res;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class AudioStream {
|
public static class AudioStream {
|
||||||
public AudioStream(String url, int format, int bandwidth, int samplingRate) {
|
|
||||||
this.url = url; this.format = format;
|
|
||||||
this.bandwidth = bandwidth; this.samplingRate = samplingRate;
|
|
||||||
}
|
|
||||||
public String url = "";
|
public String url = "";
|
||||||
public int format = -1;
|
public int format = -1;
|
||||||
public int bandwidth = -1;
|
public int bandwidth = -1;
|
||||||
public int samplingRate = -1;
|
public int samplingRate = -1;
|
||||||
|
|
||||||
|
public AudioStream(String url, int format, int bandwidth, int samplingRate) {
|
||||||
|
this.url = url; this.format = format;
|
||||||
|
this.bandwidth = bandwidth; this.samplingRate = samplingRate;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -35,7 +35,7 @@ public class VideoInfoItemViewCreator {
|
|||||||
this.inflater = inflater;
|
this.inflater = inflater;
|
||||||
}
|
}
|
||||||
|
|
||||||
public View getViewByVideoInfoItem(View convertView, ViewGroup parent, VideoInfoItem info) {
|
public View getViewByVideoInfoItem(View convertView, ViewGroup parent, VideoPreviewInfo info) {
|
||||||
ViewHolder holder;
|
ViewHolder holder;
|
||||||
if(convertView == null) {
|
if(convertView == null) {
|
||||||
convertView = inflater.inflate(R.layout.video_item, parent, false);
|
convertView = inflater.inflate(R.layout.video_item, parent, false);
|
||||||
@ -57,12 +57,12 @@ public class VideoInfoItemViewCreator {
|
|||||||
}
|
}
|
||||||
holder.itemVideoTitleView.setText(info.title);
|
holder.itemVideoTitleView.setText(info.title);
|
||||||
holder.itemUploaderView.setText(info.uploader);
|
holder.itemUploaderView.setText(info.uploader);
|
||||||
holder.itemDurationView.setText(info.duration);
|
holder.itemDurationView.setText(""+info.duration);
|
||||||
if(!info.upload_date.isEmpty()) {
|
if(!info.upload_date.isEmpty()) {
|
||||||
holder.itemUploadDateView.setText(info.upload_date);
|
holder.itemUploadDateView.setText(info.upload_date);
|
||||||
} else {
|
} else {
|
||||||
//tweak if necessary: This is a hack to prevent having white space in the layout :P
|
//tweak if necessary: This is a hack to prevent having white space in the layout :P
|
||||||
holder.itemUploadDateView.setText(info.view_count);
|
holder.itemUploadDateView.setText(""+info.view_count);
|
||||||
}
|
}
|
||||||
|
|
||||||
return convertView;
|
return convertView;
|
||||||
|
@ -7,10 +7,13 @@ import android.support.v4.app.NavUtils;
|
|||||||
import android.support.v7.app.AppCompatActivity;
|
import android.support.v7.app.AppCompatActivity;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.services.Extractor;
|
||||||
|
import org.schabi.newpipe.services.ServiceList;
|
||||||
|
import org.schabi.newpipe.services.StreamingService;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||||
@ -61,27 +64,25 @@ public class VideoItemDetailActivity extends AppCompatActivity {
|
|||||||
// this means the video was called though another app
|
// this means the video was called though another app
|
||||||
if (getIntent().getData() != null) {
|
if (getIntent().getData() != null) {
|
||||||
videoUrl = getIntent().getData().toString();
|
videoUrl = getIntent().getData().toString();
|
||||||
Log.i(TAG, "video URL passed:\"" + videoUrl + "\"");
|
//Log.i(TAG, "video URL passed:\"" + videoUrl + "\"");
|
||||||
StreamingService[] serviceList = ServiceList.getServices();
|
StreamingService[] serviceList = ServiceList.getServices();
|
||||||
Extractor extractor = null;
|
Extractor extractor = null;
|
||||||
for (int i = 0; i < serviceList.length; i++) {
|
for (int i = 0; i < serviceList.length; i++) {
|
||||||
if (serviceList[i].acceptUrl(videoUrl)) {
|
if (serviceList[i].acceptUrl(videoUrl)) {
|
||||||
arguments.putInt(VideoItemDetailFragment.STREAMING_SERVICE, i);
|
arguments.putInt(VideoItemDetailFragment.STREAMING_SERVICE, i);
|
||||||
try {
|
currentStreamingService = i;
|
||||||
currentStreamingService = i;
|
//extractor = ServiceList.getService(i).getExtractorInstance();
|
||||||
extractor = ServiceList.getService(i).getExtractorInstance();
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(extractor == null) {
|
if(currentStreamingService == -1) {
|
||||||
Toast.makeText(this, R.string.urlNotSupportedText, Toast.LENGTH_LONG)
|
Toast.makeText(this, R.string.urlNotSupportedText, Toast.LENGTH_LONG)
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
arguments.putString(VideoItemDetailFragment.VIDEO_URL,
|
//arguments.putString(VideoItemDetailFragment.VIDEO_URL,
|
||||||
extractor.getVideoUrl(extractor.getVideoId(videoUrl)));
|
// extractor.getVideoUrl(extractor.getVideoId(videoUrl)));//cleans URL
|
||||||
|
arguments.putString(VideoItemDetailFragment.VIDEO_URL, videoUrl);
|
||||||
|
|
||||||
arguments.putBoolean(VideoItemDetailFragment.AUTO_PLAY,
|
arguments.putBoolean(VideoItemDetailFragment.AUTO_PLAY,
|
||||||
PreferenceManager.getDefaultSharedPreferences(this)
|
PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
.getBoolean(getString(R.string.autoPlayThroughIntent), false));
|
.getBoolean(getString(R.string.autoPlayThroughIntent), false));
|
||||||
|
@ -32,11 +32,17 @@ import android.view.MenuItem;
|
|||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
import java.util.Calendar;
|
import java.text.ParseException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Vector;
|
import java.util.Vector;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.services.Extractor;
|
||||||
|
import org.schabi.newpipe.services.ServiceList;
|
||||||
|
import org.schabi.newpipe.services.StreamingService;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||||
@ -86,16 +92,18 @@ public class VideoItemDetailFragment extends Fragment {
|
|||||||
private class ExtractorRunnable implements Runnable {
|
private class ExtractorRunnable implements Runnable {
|
||||||
private Handler h = new Handler();
|
private Handler h = new Handler();
|
||||||
private Extractor extractor;
|
private Extractor extractor;
|
||||||
|
private StreamingService service;
|
||||||
private String videoUrl;
|
private String videoUrl;
|
||||||
|
|
||||||
public ExtractorRunnable(String videoUrl, Extractor extractor, VideoItemDetailFragment f) {
|
public ExtractorRunnable(String videoUrl, StreamingService service, VideoItemDetailFragment f) {
|
||||||
this.extractor = extractor;
|
this.service = service;
|
||||||
this.videoUrl = videoUrl;
|
this.videoUrl = videoUrl;
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
VideoInfo videoInfo = extractor.getVideoInfo(videoUrl);
|
this.extractor = service.getExtractorInstance(videoUrl);
|
||||||
|
VideoInfo videoInfo = extractor.getVideoInfo();
|
||||||
h.post(new VideoResultReturnedRunnable(videoInfo));
|
h.post(new VideoResultReturnedRunnable(videoInfo));
|
||||||
if (videoInfo.videoAvailableStatus == VideoInfo.VIDEO_AVAILABLE) {
|
if (videoInfo.videoAvailableStatus == VideoInfo.VIDEO_AVAILABLE) {
|
||||||
h.post(new SetThumbnailRunnable(
|
h.post(new SetThumbnailRunnable(
|
||||||
@ -233,15 +241,14 @@ public class VideoItemDetailFragment extends Fragment {
|
|||||||
thumbsUpView.setText(nf.format(info.like_count));
|
thumbsUpView.setText(nf.format(info.like_count));
|
||||||
thumbsDownView.setText(nf.format(info.dislike_count));
|
thumbsDownView.setText(nf.format(info.dislike_count));
|
||||||
|
|
||||||
//this is horribly convoluted
|
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
|
||||||
//TODO: find a better way to convert YYYY-MM-DD to a locale-specific date
|
Date datum = null;
|
||||||
//suggestions welcome
|
try {
|
||||||
int year = Integer.parseInt(info.upload_date.substring(0, 4));
|
datum = formatter.parse(info.upload_date);
|
||||||
int month = Integer.parseInt(info.upload_date.substring(5, 7));
|
} catch (ParseException e) {
|
||||||
int date = Integer.parseInt(info.upload_date.substring(8, 10));
|
e.printStackTrace();
|
||||||
Calendar cal = Calendar.getInstance();
|
}
|
||||||
cal.set(year, month, date);
|
|
||||||
Date datum = cal.getTime();
|
|
||||||
DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM, locale);
|
DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM, locale);
|
||||||
|
|
||||||
String localisedDate = df.format(datum);
|
String localisedDate = df.format(datum);
|
||||||
@ -251,6 +258,7 @@ public class VideoItemDetailFragment extends Fragment {
|
|||||||
descriptionView.setMovementMethod(LinkMovementMethod.getInstance());
|
descriptionView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
|
|
||||||
actionBarHandler.setVideoInfo(info.webpage_url, info.title);
|
actionBarHandler.setVideoInfo(info.webpage_url, info.title);
|
||||||
|
actionBarHandler.setStartPosition(info.startPosition);
|
||||||
|
|
||||||
// parse streams
|
// parse streams
|
||||||
Vector<VideoInfo.VideoStream> streamsToUse = new Vector<>();
|
Vector<VideoInfo.VideoStream> streamsToUse = new Vector<>();
|
||||||
@ -353,7 +361,7 @@ public class VideoItemDetailFragment extends Fragment {
|
|||||||
StreamingService streamingService = ServiceList.getService(
|
StreamingService streamingService = ServiceList.getService(
|
||||||
getArguments().getInt(STREAMING_SERVICE));
|
getArguments().getInt(STREAMING_SERVICE));
|
||||||
extractorThread = new Thread(new ExtractorRunnable(
|
extractorThread = new Thread(new ExtractorRunnable(
|
||||||
getArguments().getString(VIDEO_URL), streamingService.getExtractorInstance(), this));
|
getArguments().getString(VIDEO_URL), streamingService, this));
|
||||||
|
|
||||||
autoPlayEnabled = getArguments().getBoolean(AUTO_PLAY);
|
autoPlayEnabled = getArguments().getBoolean(AUTO_PLAY);
|
||||||
extractorThread.start();
|
extractorThread.start();
|
||||||
@ -387,17 +395,24 @@ public class VideoItemDetailFragment extends Fragment {
|
|||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
Intent intent = new Intent(activity, VideoItemListActivity.class);
|
Intent intent = new Intent(activity, VideoItemListActivity.class);
|
||||||
intent.putExtra(VideoItemListActivity.VIDEO_INFO_ITEMS, currentVideoInfo.relatedVideos);
|
//todo: find more elegant way to do this - converting from List to ArrayList sucks
|
||||||
|
ArrayList<VideoPreviewInfo> toParcel = new ArrayList<>(currentVideoInfo.relatedVideos);
|
||||||
|
//why oh why does the parcelable array put method have to be so damn specific
|
||||||
|
// about the class of its argument?
|
||||||
|
//why not a List<? extends Parcelable>?
|
||||||
|
intent.putParcelableArrayListExtra(VideoItemListActivity.VIDEO_INFO_ITEMS, toParcel);
|
||||||
activity.startActivity(intent);
|
activity.startActivity(intent);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**Returns the java.util.Locale object which corresponds to the locale set in NewPipe's preferences.
|
||||||
|
* Currently not affected by the device's locale.*/
|
||||||
public Locale getPreferredLocale() {
|
public Locale getPreferredLocale() {
|
||||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext());
|
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||||
String languageKey = getContext().getString(R.string.searchLanguage);
|
String languageKey = getContext().getString(R.string.searchLanguage);
|
||||||
String languageCode = "en";//i know the following lines defaults languageCode to "en", but java is picky about uninitialised values
|
String languageCode = "en";//i know the following line defaults languageCode to "en", but java is picky about uninitialised values
|
||||||
languageCode = sp.getString(languageKey, "en");
|
languageCode = sp.getString(languageKey, "en");
|
||||||
|
|
||||||
if(languageCode.length() == 2) {
|
if(languageCode.length() == 2) {
|
||||||
|
@ -3,21 +3,18 @@ package org.schabi.newpipe;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Parcel;
|
|
||||||
import android.os.Parcelable;
|
|
||||||
import android.support.v4.app.NavUtils;
|
import android.support.v4.app.NavUtils;
|
||||||
import android.support.v7.app.AppCompatActivity;
|
import android.support.v7.app.AppCompatActivity;
|
||||||
import android.support.v7.widget.SearchView;
|
import android.support.v7.widget.SearchView;
|
||||||
import android.util.Log;
|
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.inputmethod.InputMethodManager;
|
import android.view.inputmethod.InputMethodManager;
|
||||||
import android.widget.ImageView;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
|
import org.schabi.newpipe.services.ServiceList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||||
@ -116,7 +113,7 @@ public class VideoItemListActivity extends AppCompatActivity
|
|||||||
|
|
||||||
if(arguments != null) {
|
if(arguments != null) {
|
||||||
//Parcelable[] p = arguments.getParcelableArray(VIDEO_INFO_ITEMS);
|
//Parcelable[] p = arguments.getParcelableArray(VIDEO_INFO_ITEMS);
|
||||||
ArrayList<VideoInfoItem> p = arguments.getParcelableArrayList(VIDEO_INFO_ITEMS);
|
ArrayList<VideoPreviewInfo> p = arguments.getParcelableArrayList(VIDEO_INFO_ITEMS);
|
||||||
if(p != null) {
|
if(p != null) {
|
||||||
mode = PRESENT_VIDEOS_MODE;
|
mode = PRESENT_VIDEOS_MODE;
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
|
@ -15,10 +15,12 @@ import android.widget.ListView;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Vector;
|
import java.util.Vector;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.services.SearchEngine;
|
||||||
|
import org.schabi.newpipe.services.StreamingService;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||||
@ -119,9 +121,9 @@ public class VideoItemListFragment extends ListFragment {
|
|||||||
Handler h = new Handler();
|
Handler h = new Handler();
|
||||||
private volatile boolean run = true;
|
private volatile boolean run = true;
|
||||||
private int requestId;
|
private int requestId;
|
||||||
public LoadThumbsRunnable(Vector<VideoInfoItem> videoList,
|
public LoadThumbsRunnable(Vector<VideoPreviewInfo> videoList,
|
||||||
Vector<Boolean> downloadedList, int requestId) {
|
Vector<Boolean> downloadedList, int requestId) {
|
||||||
for(VideoInfoItem item : videoList) {
|
for(VideoPreviewInfo item : videoList) {
|
||||||
thumbnailUrlList.add(item.thumbnail_url);
|
thumbnailUrlList.add(item.thumbnail_url);
|
||||||
}
|
}
|
||||||
this.downloadedList = downloadedList;
|
this.downloadedList = downloadedList;
|
||||||
@ -168,7 +170,7 @@ public class VideoItemListFragment extends ListFragment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void present(List<VideoInfoItem> videoList) {
|
public void present(List<VideoPreviewInfo> videoList) {
|
||||||
mode = PRESENT_VIDEOS_MODE;
|
mode = PRESENT_VIDEOS_MODE;
|
||||||
setListShown(true);
|
setListShown(true);
|
||||||
getListView().smoothScrollToPosition(0);
|
getListView().smoothScrollToPosition(0);
|
||||||
@ -220,7 +222,7 @@ public class VideoItemListFragment extends ListFragment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateList(List<VideoInfoItem> list) {
|
private void updateList(List<VideoPreviewInfo> list) {
|
||||||
try {
|
try {
|
||||||
videoListAdapter.addVideoList(list);
|
videoListAdapter.addVideoList(list);
|
||||||
terminateThreads();
|
terminateThreads();
|
||||||
|
@ -37,7 +37,7 @@ public class VideoListAdapter extends BaseAdapter {
|
|||||||
|
|
||||||
private Context context;
|
private Context context;
|
||||||
private VideoInfoItemViewCreator viewCreator;
|
private VideoInfoItemViewCreator viewCreator;
|
||||||
private Vector<VideoInfoItem> videoList = new Vector<>();
|
private Vector<VideoPreviewInfo> videoList = new Vector<>();
|
||||||
private Vector<Boolean> downloadedThumbnailList = new Vector<>();
|
private Vector<Boolean> downloadedThumbnailList = new Vector<>();
|
||||||
VideoItemListFragment videoListFragment;
|
VideoItemListFragment videoListFragment;
|
||||||
ListView listView;
|
ListView listView;
|
||||||
@ -49,7 +49,7 @@ public class VideoListAdapter extends BaseAdapter {
|
|||||||
this.context = context;
|
this.context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addVideoList(List<VideoInfoItem> videos) {
|
public void addVideoList(List<VideoPreviewInfo> videos) {
|
||||||
videoList.addAll(videos);
|
videoList.addAll(videos);
|
||||||
for(int i = 0; i < videos.size(); i++) {
|
for(int i = 0; i < videos.size(); i++) {
|
||||||
downloadedThumbnailList.add(false);
|
downloadedThumbnailList.add(false);
|
||||||
@ -63,7 +63,7 @@ public class VideoListAdapter extends BaseAdapter {
|
|||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Vector<VideoInfoItem> getVideoList() {
|
public Vector<VideoPreviewInfo> getVideoList() {
|
||||||
return videoList;
|
return videoList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import android.os.Parcelable;
|
|||||||
* Created by Christian Schabesberger on 26.08.15.
|
* Created by Christian Schabesberger on 26.08.15.
|
||||||
*
|
*
|
||||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||||
* VideoInfoItem.java is part of NewPipe.
|
* VideoPreviewInfo.java is part of NewPipe.
|
||||||
*
|
*
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
* NewPipe is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@ -25,19 +25,9 @@ import android.os.Parcelable;
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**Info object for previews of unopened videos, eg search results, related videos*/
|
/**Info object for previews of unopened videos, eg search results, related videos*/
|
||||||
public class VideoInfoItem implements Parcelable {
|
public class VideoPreviewInfo extends AbstractVideoInfo implements Parcelable {
|
||||||
public String id = "";
|
|
||||||
public String title = "";
|
|
||||||
public String uploader = "";
|
|
||||||
public String thumbnail_url = "";
|
|
||||||
public Bitmap thumbnail = null;
|
|
||||||
public String webpage_url = "";
|
|
||||||
public String upload_date = "";
|
|
||||||
public String view_count = "";
|
|
||||||
|
|
||||||
public String duration = "";
|
public String duration = "";
|
||||||
|
protected VideoPreviewInfo(Parcel in) {
|
||||||
protected VideoInfoItem(Parcel in) {
|
|
||||||
id = in.readString();
|
id = in.readString();
|
||||||
title = in.readString();
|
title = in.readString();
|
||||||
uploader = in.readString();
|
uploader = in.readString();
|
||||||
@ -46,10 +36,10 @@ public class VideoInfoItem implements Parcelable {
|
|||||||
thumbnail = (Bitmap) in.readValue(Bitmap.class.getClassLoader());
|
thumbnail = (Bitmap) in.readValue(Bitmap.class.getClassLoader());
|
||||||
webpage_url = in.readString();
|
webpage_url = in.readString();
|
||||||
upload_date = in.readString();
|
upload_date = in.readString();
|
||||||
view_count = in.readString();
|
view_count = in.readLong();
|
||||||
}
|
}
|
||||||
|
|
||||||
public VideoInfoItem() {
|
public VideoPreviewInfo() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,19 +58,19 @@ public class VideoInfoItem implements Parcelable {
|
|||||||
dest.writeValue(thumbnail);
|
dest.writeValue(thumbnail);
|
||||||
dest.writeString(webpage_url);
|
dest.writeString(webpage_url);
|
||||||
dest.writeString(upload_date);
|
dest.writeString(upload_date);
|
||||||
dest.writeString(view_count);
|
dest.writeLong(view_count);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public static final Parcelable.Creator<VideoInfoItem> CREATOR = new Parcelable.Creator<VideoInfoItem>() {
|
public static final Parcelable.Creator<VideoPreviewInfo> CREATOR = new Parcelable.Creator<VideoPreviewInfo>() {
|
||||||
@Override
|
@Override
|
||||||
public VideoInfoItem createFromParcel(Parcel in) {
|
public VideoPreviewInfo createFromParcel(Parcel in) {
|
||||||
return new VideoInfoItem(in);
|
return new VideoPreviewInfo(in);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public VideoInfoItem[] newArray(int size) {
|
public VideoPreviewInfo[] newArray(int size) {
|
||||||
return new VideoInfoItem[size];
|
return new VideoPreviewInfo[size];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
115
app/src/main/java/org/schabi/newpipe/services/Extractor.java
Normal file
115
app/src/main/java/org/schabi/newpipe/services/Extractor.java
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
package org.schabi.newpipe.services;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by Christian Schabesberger on 10.08.15.
|
||||||
|
*
|
||||||
|
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||||
|
* Extractor.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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import org.schabi.newpipe.VideoInfo;
|
||||||
|
|
||||||
|
/**Scrapes information from a video streaming service (eg, YouTube).*/
|
||||||
|
public abstract class Extractor {
|
||||||
|
public String pageUrl;
|
||||||
|
public VideoInfo videoInfo;
|
||||||
|
|
||||||
|
public Extractor(String url) {
|
||||||
|
this.pageUrl = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**Fills out the video info fields which are common to all services.
|
||||||
|
* Probably needs to be overridden by subclasses*/
|
||||||
|
public VideoInfo getVideoInfo()
|
||||||
|
{
|
||||||
|
if(videoInfo == null) {
|
||||||
|
videoInfo = new VideoInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(videoInfo.webpage_url.isEmpty()) {
|
||||||
|
videoInfo.webpage_url = pageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(videoInfo.title.isEmpty()) {
|
||||||
|
videoInfo.title = getTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(videoInfo.duration < 1) {
|
||||||
|
videoInfo.duration = getLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if(videoInfo.uploader.isEmpty()) {
|
||||||
|
videoInfo.uploader = getUploader();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(videoInfo.description.isEmpty()) {
|
||||||
|
videoInfo.description = getDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(videoInfo.view_count == -1) {
|
||||||
|
videoInfo.view_count = getViews();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(videoInfo.upload_date.isEmpty()) {
|
||||||
|
videoInfo.upload_date = getUploadDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(videoInfo.thumbnail_url.isEmpty()) {
|
||||||
|
videoInfo.thumbnail_url = getThumbnailUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(videoInfo.id.isEmpty()) {
|
||||||
|
videoInfo.id = getVideoId(pageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Load and extract audio*/
|
||||||
|
if(videoInfo.audioStreams == null) {
|
||||||
|
videoInfo.audioStreams = getAudioStreams();
|
||||||
|
}
|
||||||
|
/** Extract video stream url*/
|
||||||
|
if(videoInfo.videoStreams == null) {
|
||||||
|
videoInfo.videoStreams = getVideoStreams();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(videoInfo.uploader_thumbnail_url.isEmpty()) {
|
||||||
|
videoInfo.uploader_thumbnail_url = getUploaderThumbnailUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(videoInfo.startPosition < 0) {
|
||||||
|
videoInfo.startPosition = getTimeStamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Bitmap thumbnail = null;
|
||||||
|
//Bitmap uploader_thumbnail = null;
|
||||||
|
//int videoAvailableStatus = VIDEO_AVAILABLE;
|
||||||
|
return videoInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract String getVideoUrl(String videoId);
|
||||||
|
public abstract String getVideoId(String siteUrl);
|
||||||
|
public abstract int getTimeStamp();
|
||||||
|
public abstract String getTitle();
|
||||||
|
public abstract String getDescription();
|
||||||
|
public abstract String getUploader();
|
||||||
|
public abstract int getLength();
|
||||||
|
public abstract int getViews();
|
||||||
|
public abstract String getUploadDate();
|
||||||
|
public abstract String getThumbnailUrl();
|
||||||
|
public abstract String getUploaderThumbnailUrl();
|
||||||
|
public abstract VideoInfo.AudioStream[] getAudioStreams();
|
||||||
|
public abstract VideoInfo.VideoStream[] getVideoStreams();
|
||||||
|
}
|
@ -1,4 +1,6 @@
|
|||||||
package org.schabi.newpipe;
|
package org.schabi.newpipe.services;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.VideoPreviewInfo;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Vector;
|
import java.util.Vector;
|
||||||
@ -29,7 +31,7 @@ public interface SearchEngine {
|
|||||||
class Result {
|
class Result {
|
||||||
public String errorMessage = "";
|
public String errorMessage = "";
|
||||||
public String suggestion = "";
|
public String suggestion = "";
|
||||||
public Vector<VideoInfoItem> resultList = new Vector<>();
|
public Vector<VideoPreviewInfo> resultList = new Vector<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
ArrayList<String> suggestionList(String query);
|
ArrayList<String> suggestionList(String query);
|
@ -1,8 +1,8 @@
|
|||||||
package org.schabi.newpipe;
|
package org.schabi.newpipe.services;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import org.schabi.newpipe.youtube.YoutubeService;
|
import org.schabi.newpipe.services.youtube.YoutubeService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Christian Schabesberger on 23.08.15.
|
* Created by Christian Schabesberger on 23.08.15.
|
||||||
@ -24,6 +24,8 @@ import org.schabi.newpipe.youtube.YoutubeService;
|
|||||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**Provides access to the video streaming services supported by NewPipe.
|
||||||
|
* Currently only Youtube until the API becomes more stable.*/
|
||||||
public class ServiceList {
|
public class ServiceList {
|
||||||
private static final String TAG = ServiceList.class.toString();
|
private static final String TAG = ServiceList.class.toString();
|
||||||
private static final StreamingService[] services = {
|
private static final StreamingService[] services = {
|
@ -1,4 +1,4 @@
|
|||||||
package org.schabi.newpipe;
|
package org.schabi.newpipe.services;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Christian Schabesberger on 23.08.15.
|
* Created by Christian Schabesberger on 23.08.15.
|
||||||
@ -25,7 +25,7 @@ public interface StreamingService {
|
|||||||
public String name = "";
|
public String name = "";
|
||||||
}
|
}
|
||||||
ServiceInfo getServiceInfo();
|
ServiceInfo getServiceInfo();
|
||||||
Extractor getExtractorInstance();
|
Extractor getExtractorInstance(String url);
|
||||||
SearchEngine getSearchEngineInstance();
|
SearchEngine getSearchEngineInstance();
|
||||||
|
|
||||||
/**When a VIEW_ACTION is caught this function will test if the url delivered within the calling
|
/**When a VIEW_ACTION is caught this function will test if the url delivered within the calling
|
@ -1,8 +1,9 @@
|
|||||||
package org.schabi.newpipe.youtube;
|
package org.schabi.newpipe.services.youtube;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.Xml;
|
import android.util.Xml;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
import org.jsoup.Jsoup;
|
import org.jsoup.Jsoup;
|
||||||
import org.jsoup.nodes.Document;
|
import org.jsoup.nodes.Document;
|
||||||
@ -12,14 +13,13 @@ import org.mozilla.javascript.Context;
|
|||||||
import org.mozilla.javascript.Function;
|
import org.mozilla.javascript.Function;
|
||||||
import org.mozilla.javascript.ScriptableObject;
|
import org.mozilla.javascript.ScriptableObject;
|
||||||
import org.schabi.newpipe.Downloader;
|
import org.schabi.newpipe.Downloader;
|
||||||
import org.schabi.newpipe.Extractor;
|
import org.schabi.newpipe.services.Extractor;
|
||||||
import org.schabi.newpipe.MediaFormat;
|
import org.schabi.newpipe.MediaFormat;
|
||||||
import org.schabi.newpipe.VideoInfo;
|
import org.schabi.newpipe.VideoInfo;
|
||||||
import org.schabi.newpipe.VideoInfoItem;
|
import org.schabi.newpipe.VideoPreviewInfo;
|
||||||
import org.xmlpull.v1.XmlPullParser;
|
import org.xmlpull.v1.XmlPullParser;
|
||||||
|
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URLDecoder;
|
import java.net.URLDecoder;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -47,14 +47,225 @@ import java.util.regex.Pattern;
|
|||||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class YoutubeExtractor implements Extractor {
|
public class YoutubeExtractor extends Extractor {
|
||||||
|
|
||||||
private static final String TAG = YoutubeExtractor.class.toString();
|
private static final String TAG = YoutubeExtractor.class.toString();
|
||||||
|
private String pageContents;
|
||||||
|
private Document doc;
|
||||||
|
private JSONObject jsonObj;
|
||||||
|
private JSONObject playerArgs;
|
||||||
|
|
||||||
// These lists only contain itag formats that are supported by the common Android Video player.
|
// static values
|
||||||
// How ever if you are heading for a list showing all itag formats look at
|
private static final String DECRYPTION_FUNC_NAME="decrypt";
|
||||||
// https://github.com/rg3/youtube-dl/issues/1687
|
|
||||||
|
|
||||||
|
// cached values
|
||||||
|
private static volatile String decryptionCode = "";
|
||||||
|
|
||||||
|
|
||||||
|
public YoutubeExtractor(String pageUrl) {
|
||||||
|
super(pageUrl);//most common videoInfo fields are now set in our superclass, for all services
|
||||||
|
pageContents = Downloader.download(cleanUrl(pageUrl));
|
||||||
|
doc = Jsoup.parse(pageContents, pageUrl);
|
||||||
|
|
||||||
|
//attempt to load the youtube js player JSON arguments
|
||||||
|
try {
|
||||||
|
String jsonString = matchGroup1("ytplayer.config\\s*=\\s*(\\{.*?\\});", pageContents);
|
||||||
|
jsonObj = new JSONObject(jsonString);
|
||||||
|
playerArgs = jsonObj.getJSONObject("args");
|
||||||
|
|
||||||
|
} catch (Exception e) {//if this fails, the video is most likely not available.
|
||||||
|
// Determining why is done later.
|
||||||
|
videoInfo.videoAvailableStatus = VideoInfo.VIDEO_UNAVAILABLE;
|
||||||
|
Log.d(TAG, "Could not load JSON data for Youtube video \""+pageUrl+"\". This most likely means the video is unavailable");
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------------------------
|
||||||
|
// load and parse description code, if it isn't already initialised
|
||||||
|
//----------------------------------
|
||||||
|
if (decryptionCode.isEmpty()) {
|
||||||
|
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 = jsonObj.getJSONObject("assets");
|
||||||
|
String playerUrl = ytAssets.getString("js");
|
||||||
|
|
||||||
|
if (playerUrl.startsWith("//")) {
|
||||||
|
playerUrl = "https:" + playerUrl;
|
||||||
|
}
|
||||||
|
decryptionCode = loadDecryptionCode(playerUrl);
|
||||||
|
} catch (Exception e){
|
||||||
|
Log.d(TAG, "Could not load decryption code for the Youtube service.");
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTitle() {
|
||||||
|
try {//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");
|
||||||
|
} try { // fall through to fall-back
|
||||||
|
return doc.select("meta[name=title]").attr("content");
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "failed permanently to load title.");
|
||||||
|
e.printStackTrace();
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
try {
|
||||||
|
return doc.select("p[id=\"eow-description\"]").first().html();
|
||||||
|
} catch (Exception e) {//todo: add fallback method
|
||||||
|
Log.e(TAG, "failed to load description.");
|
||||||
|
e.printStackTrace();
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUploader() {
|
||||||
|
try {//json player args method
|
||||||
|
return playerArgs.getString("author");
|
||||||
|
} catch(JSONException je) {
|
||||||
|
je.printStackTrace();
|
||||||
|
Log.w(TAG, "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();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
Log.e(TAG, "failed permanently to load uploader name.");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getLength() {
|
||||||
|
try {
|
||||||
|
return playerArgs.getInt("length_seconds");
|
||||||
|
} catch (JSONException je) {//todo: find fallback method
|
||||||
|
Log.e(TAG, "failed to load video duration from JSON args");
|
||||||
|
je.printStackTrace();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getViews() {
|
||||||
|
try {
|
||||||
|
String viewCountString = doc.select("meta[itemprop=interactionCount]").attr("content");
|
||||||
|
return Integer.parseInt(viewCountString);
|
||||||
|
} catch (Exception e) {//todo: find fallback method
|
||||||
|
Log.e(TAG, "failed to number of views");
|
||||||
|
e.printStackTrace();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUploadDate() {
|
||||||
|
try {
|
||||||
|
return doc.select("meta[itemprop=datePublished]").attr("content");
|
||||||
|
} catch (Exception e) {//todo: add fallback method
|
||||||
|
Log.e(TAG, "failed to get upload date.");
|
||||||
|
e.printStackTrace();
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getThumbnailUrl() {
|
||||||
|
//first attempt getting a small image version
|
||||||
|
//in the html extracting part we try to get a thumbnail with a higher resolution
|
||||||
|
// Try to get high resolution thumbnail if it fails use low res from the player instead
|
||||||
|
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");
|
||||||
|
//fall through to fallback
|
||||||
|
} try {
|
||||||
|
return playerArgs.getString("thumbnail_url");
|
||||||
|
} catch (JSONException je) {
|
||||||
|
je.printStackTrace();
|
||||||
|
Log.w(TAG, "failed to extract thumbnail URL from JSON args; trying to extract it from HTML");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUploaderThumbnailUrl() {
|
||||||
|
try {
|
||||||
|
return doc.select("a[class*=\"yt-user-photo\"]").first()
|
||||||
|
.select("img").first()
|
||||||
|
.attr("abs:data-thumb");
|
||||||
|
} catch (Exception e) {//todo: add fallback method
|
||||||
|
Log.e(TAG, "failed to get uploader thumbnail URL.");
|
||||||
|
e.printStackTrace();
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public VideoInfo.AudioStream[] getAudioStreams() {
|
||||||
|
try {
|
||||||
|
String dashManifest = playerArgs.getString("dashmpd");
|
||||||
|
return parseDashManifest(dashManifest, decryptionCode);
|
||||||
|
|
||||||
|
} catch (NullPointerException e) {
|
||||||
|
Log.e(TAG, "Could not find \"dashmpd\" upon the player args (maybe no dash manifest available).");
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return new VideoInfo.AudioStream[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public VideoInfo.VideoStream[] getVideoStreams() {
|
||||||
|
try{
|
||||||
|
//------------------------------------
|
||||||
|
// extract video stream url
|
||||||
|
//------------------------------------
|
||||||
|
String encoded_url_map = playerArgs.getString("url_encoded_fmt_stream_map");
|
||||||
|
Vector<VideoInfo.VideoStream> videoStreams = new Vector<>();
|
||||||
|
for(String url_data_str : encoded_url_map.split(",")) {
|
||||||
|
Map<String, String> tags = new HashMap<>();
|
||||||
|
for(String raw_tag : Parser.unescapeEntities(url_data_str, true).split("&")) {
|
||||||
|
String[] split_tag = raw_tag.split("=");
|
||||||
|
tags.put(split_tag[0], split_tag[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
int itag = Integer.parseInt(tags.get("itag"));
|
||||||
|
String streamUrl = URLDecoder.decode(tags.get("url"), "UTF-8");
|
||||||
|
|
||||||
|
// if video has a signature: decrypt it and add it to the url
|
||||||
|
if(tags.get("s") != null) {
|
||||||
|
streamUrl = streamUrl + "&signature=" + decryptSignature(tags.get("s"), decryptionCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(resolveFormat(itag) != -1) {
|
||||||
|
videoStreams.add(new VideoInfo.VideoStream(
|
||||||
|
streamUrl,
|
||||||
|
resolveFormat(itag),
|
||||||
|
resolveResolutionString(itag)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return videoStreams.toArray(new VideoInfo.VideoStream[videoStreams.size()]);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Failed to get video stream");
|
||||||
|
e.printStackTrace();
|
||||||
|
return new VideoInfo.VideoStream[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**These lists only contain itag formats that are supported by the common Android Video player.
|
||||||
|
However if you are looking for a list showing all itag formats, look at
|
||||||
|
https://github.com/rg3/youtube-dl/issues/1687 */
|
||||||
public static int resolveFormat(int itag) {
|
public static int resolveFormat(int itag) {
|
||||||
switch(itag) {
|
switch(itag) {
|
||||||
// video
|
// video
|
||||||
@ -92,68 +303,28 @@ public class YoutubeExtractor implements Extractor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// static values
|
|
||||||
private static final String DECRYPTION_FUNC_NAME="decrypt";
|
|
||||||
|
|
||||||
// cached values
|
|
||||||
private static volatile String decryptionCode = "";
|
|
||||||
|
|
||||||
public void initService(String site) {
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
// Star Wars Kid is used as a dummy video, in order to download the youtube player.
|
|
||||||
//String site = Downloader.download("https://www.youtube.com/watch?v=HPPj6viIBmU");
|
|
||||||
//-------------------------------------
|
|
||||||
// extracting form player args
|
|
||||||
//-------------------------------------
|
|
||||||
try {
|
|
||||||
String jsonString = matchGroup1("ytplayer.config\\s*=\\s*(\\{.*?\\});", site);
|
|
||||||
JSONObject jsonObj = new JSONObject(jsonString);
|
|
||||||
|
|
||||||
//----------------------------------
|
|
||||||
// load and parse description code
|
|
||||||
//----------------------------------
|
|
||||||
if (decryptionCode.isEmpty()) {
|
|
||||||
JSONObject ytAssets = jsonObj.getJSONObject("assets");
|
|
||||||
String playerUrl = ytAssets.getString("js");
|
|
||||||
if (playerUrl.startsWith("//")) {
|
|
||||||
playerUrl = "https:" + playerUrl;
|
|
||||||
}
|
|
||||||
decryptionCode = loadDecryptionCode(playerUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e){
|
|
||||||
Log.d(TAG, "Could not initialize the extractor of the Youtube service.");
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getVideoId(String videoUrl) {
|
public String getVideoId(String url) {
|
||||||
String id = "";
|
String id;
|
||||||
Pattern pat;
|
String pat;
|
||||||
|
|
||||||
if(videoUrl.contains("youtube")) {
|
if(url.contains("youtube")) {
|
||||||
pat = Pattern.compile("youtube\\.com/watch\\?v=([\\-a-zA-Z0-9_]{11})");
|
pat = "youtube\\.com/watch\\?v=([\\-a-zA-Z0-9_]{11})";
|
||||||
}
|
}
|
||||||
else if(videoUrl.contains("youtu.be")) {
|
else if(url.contains("youtu.be")) {
|
||||||
pat = Pattern.compile("youtu\\.be/([a-zA-Z0-9_-]{11})");
|
pat = "youtu\\.be/([a-zA-Z0-9_-]{11})";
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Log.e(TAG, "Error could not parse url: " + videoUrl);
|
Log.e(TAG, "Error could not parse url: " + url);
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
Matcher mat = pat.matcher(videoUrl);
|
id = matchGroup1(pat, url);
|
||||||
boolean foundMatch = mat.find();
|
if(!id.isEmpty()){
|
||||||
if(foundMatch){
|
Log.i(TAG, "string \""+url+"\" matches!");
|
||||||
id = mat.group(1);
|
return id;
|
||||||
Log.i(TAG, "string \""+videoUrl+"\" matches!");
|
|
||||||
}
|
}
|
||||||
Log.i(TAG, "string \""+videoUrl+"\" does not match.");
|
Log.i(TAG, "string \""+url+"\" does not match.");
|
||||||
return id;
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -161,95 +332,47 @@ public class YoutubeExtractor implements Extractor {
|
|||||||
return "https://www.youtube.com/watch?v=" + videoId;
|
return "https://www.youtube.com/watch?v=" + videoId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**Attempts to parse (and return) the offset to start playing the video from.
|
||||||
|
* @return the offset (in seconds), or 0 if no timestamp is found.*/
|
||||||
@Override
|
@Override
|
||||||
public VideoInfo getVideoInfo(String siteUrl) {
|
public int getTimeStamp(){
|
||||||
String site = Downloader.download(siteUrl);
|
String timeStamp = matchGroup1("((#|&)t=\\d{0,3}h?\\d{0,3}m?\\d{1,3}s?)", pageUrl);
|
||||||
VideoInfo videoInfo = new VideoInfo();
|
|
||||||
|
|
||||||
Document doc = Jsoup.parse(site, siteUrl);
|
//TODO: test this
|
||||||
|
if(!timeStamp.isEmpty()) {
|
||||||
|
String secondsString = matchGroup1("(\\d{1,3})s", timeStamp);
|
||||||
|
String minutesString = matchGroup1("(\\d{1,3})m", timeStamp);
|
||||||
|
String hoursString = matchGroup1("(\\d{1,3})h", timeStamp);
|
||||||
|
|
||||||
videoInfo.id = matchGroup1("v=([0-9a-zA-Z_-]{11})", siteUrl);
|
if(secondsString.isEmpty()//if nothing was got,
|
||||||
|
&& minutesString.isEmpty()//treat as unlabelled seconds
|
||||||
|
&& hoursString.isEmpty())
|
||||||
|
secondsString = matchGroup1("t=(\\d{1,3})", timeStamp);
|
||||||
|
|
||||||
|
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!
|
||||||
|
Log.d(TAG, "derived timestamp value:"+ret);
|
||||||
|
return ret;
|
||||||
|
//the ordering varies internationally
|
||||||
|
}//else, return default 0
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public VideoInfo getVideoInfo() {
|
||||||
|
videoInfo = super.getVideoInfo();
|
||||||
|
//todo: replace this with a call to getVideoId, if possible
|
||||||
|
videoInfo.id = matchGroup1("v=([0-9a-zA-Z_-]{11})", pageUrl);
|
||||||
|
|
||||||
videoInfo.age_limit = 0;
|
videoInfo.age_limit = 0;
|
||||||
videoInfo.webpage_url = siteUrl;
|
|
||||||
|
|
||||||
initService(site);
|
//average rating
|
||||||
|
|
||||||
//-------------------------------------
|
|
||||||
// extracting form player args
|
|
||||||
//-------------------------------------
|
|
||||||
JSONObject playerArgs = null;
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
String jsonString = matchGroup1("ytplayer.config\\s*=\\s*(\\{.*?\\});", site);
|
|
||||||
JSONObject jsonObj = new JSONObject(jsonString);
|
|
||||||
playerArgs = jsonObj.getJSONObject("args");
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
// If we fail in this part the video is most likely not available.
|
|
||||||
// Determining why is done later.
|
|
||||||
videoInfo.videoAvailableStatus = VideoInfo.VIDEO_UNAVAILABLE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//-----------------------
|
|
||||||
// load and extract audio
|
|
||||||
//-----------------------
|
|
||||||
try {
|
try {
|
||||||
String dashManifest = playerArgs.getString("dashmpd");
|
|
||||||
videoInfo.audioStreams = parseDashManifest(dashManifest, decryptionCode);
|
|
||||||
|
|
||||||
} catch (NullPointerException e) {
|
|
||||||
Log.e(TAG, "Could not find \"dashmpd\" upon the player args (maybe no dash manifest available).");
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
//--------------------------------------------
|
|
||||||
// extract general information about the video
|
|
||||||
//--------------------------------------------
|
|
||||||
|
|
||||||
videoInfo.uploader = playerArgs.getString("author");
|
|
||||||
videoInfo.title = playerArgs.getString("title");
|
|
||||||
//first attempt getting a small image version
|
|
||||||
//in the html extracting part we try to get a thumbnail with a higher resolution
|
|
||||||
videoInfo.thumbnail_url = playerArgs.getString("thumbnail_url");
|
|
||||||
videoInfo.duration = playerArgs.getInt("length_seconds");
|
|
||||||
videoInfo.average_rating = playerArgs.getString("avg_rating");
|
videoInfo.average_rating = playerArgs.getString("avg_rating");
|
||||||
|
} catch (JSONException e) {
|
||||||
//------------------------------------
|
|
||||||
// extract video stream url
|
|
||||||
//------------------------------------
|
|
||||||
String encoded_url_map = playerArgs.getString("url_encoded_fmt_stream_map");
|
|
||||||
Vector<VideoInfo.VideoStream> videoStreams = new Vector<>();
|
|
||||||
for(String url_data_str : encoded_url_map.split(",")) {
|
|
||||||
Map<String, String> tags = new HashMap<>();
|
|
||||||
for(String raw_tag : Parser.unescapeEntities(url_data_str, true).split("&")) {
|
|
||||||
String[] split_tag = raw_tag.split("=");
|
|
||||||
tags.put(split_tag[0], split_tag[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
int itag = Integer.parseInt(tags.get("itag"));
|
|
||||||
String streamUrl = URLDecoder.decode(tags.get("url"), "UTF-8");
|
|
||||||
|
|
||||||
// if video has a signature: decrypt it and add it to the url
|
|
||||||
if(tags.get("s") != null) {
|
|
||||||
streamUrl = streamUrl + "&signature=" + decryptSignature(tags.get("s"), decryptionCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(resolveFormat(itag) != -1) {
|
|
||||||
videoStreams.add(new VideoInfo.VideoStream(
|
|
||||||
streamUrl,
|
|
||||||
resolveFormat(itag),
|
|
||||||
resolveResolutionString(itag)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
videoInfo.videoStreams =
|
|
||||||
videoStreams.toArray(new VideoInfo.VideoStream[videoStreams.size()]);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -257,7 +380,6 @@ public class YoutubeExtractor implements Extractor {
|
|||||||
// extracting information from html page
|
// extracting information from html page
|
||||||
//---------------------------------------
|
//---------------------------------------
|
||||||
|
|
||||||
|
|
||||||
// Determine what went wrong when the Video is not available
|
// Determine what went wrong when the Video is not available
|
||||||
if(videoInfo.videoAvailableStatus == VideoInfo.VIDEO_UNAVAILABLE) {
|
if(videoInfo.videoAvailableStatus == VideoInfo.VIDEO_UNAVAILABLE) {
|
||||||
if(doc.select("h1[id=\"unavailable-message\"]").first().text().contains("GEMA")) {
|
if(doc.select("h1[id=\"unavailable-message\"]").first().text().contains("GEMA")) {
|
||||||
@ -265,22 +387,6 @@ public class YoutubeExtractor implements Extractor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to get high resolution thumbnail if it fails use low res from the player instead
|
|
||||||
try {
|
|
||||||
videoInfo.thumbnail_url = doc.select("link[itemprop=\"thumbnailUrl\"]").first()
|
|
||||||
.attr("abs:href");
|
|
||||||
} catch(Exception e) {
|
|
||||||
Log.i(TAG, "Could not find high res Thumbnail. Using low res instead");
|
|
||||||
}
|
|
||||||
|
|
||||||
// upload date
|
|
||||||
videoInfo.upload_date = doc.select("meta[itemprop=datePublished]").attr("content");
|
|
||||||
|
|
||||||
//TODO: Format date locale-specifically
|
|
||||||
|
|
||||||
|
|
||||||
// description
|
|
||||||
videoInfo.description = doc.select("p[id=\"eow-description\"]").first().html();
|
|
||||||
String likesString = "";
|
String likesString = "";
|
||||||
String dislikesString = "";
|
String dislikesString = "";
|
||||||
try {
|
try {
|
||||||
@ -303,31 +409,25 @@ public class YoutubeExtractor implements Extractor {
|
|||||||
videoInfo.dislike_count = 0;
|
videoInfo.dislike_count = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// uploader thumbnail
|
|
||||||
videoInfo.uploader_thumbnail_url = doc.select("a[class*=\"yt-user-photo\"]").first()
|
|
||||||
.select("img").first()
|
|
||||||
.attr("abs:data-thumb");
|
|
||||||
|
|
||||||
// view count TODO: locale-specific formatting
|
|
||||||
String viewCountString = doc.select("meta[itemprop=interactionCount]").attr("content");
|
|
||||||
videoInfo.view_count = Integer.parseInt(viewCountString);
|
|
||||||
|
|
||||||
// next video
|
// next video
|
||||||
videoInfo.nextVideo = extractVideoInfoItem(doc.select("div[class=\"watch-sidebar-section\"]").first()
|
videoInfo.nextVideo = extractVideoPreviewInfo(doc.select("div[class=\"watch-sidebar-section\"]").first()
|
||||||
.select("li").first());
|
.select("li").first());
|
||||||
|
|
||||||
// related videos
|
// related videos
|
||||||
Vector<VideoInfoItem> relatedVideos = new Vector<>();
|
Vector<VideoPreviewInfo> relatedVideos = new Vector<>();
|
||||||
for(Element li : doc.select("ul[id=\"watch-related\"]").first().children()) {
|
for(Element li : doc.select("ul[id=\"watch-related\"]").first().children()) {
|
||||||
// first check if we have a playlist. If so leave them out
|
// first check if we have a playlist. If so leave them out
|
||||||
if(li.select("a[class*=\"content-link\"]").first() != null) {
|
if(li.select("a[class*=\"content-link\"]").first() != null) {
|
||||||
relatedVideos.add(extractVideoInfoItem(li));
|
relatedVideos.add(extractVideoPreviewInfo(li));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
videoInfo.relatedVideos = relatedVideos.toArray(new VideoInfoItem[relatedVideos.size()]);
|
//todo: replace conversion
|
||||||
|
videoInfo.relatedVideos = relatedVideos;
|
||||||
|
//videoInfo.relatedVideos = relatedVideos.toArray(new VideoPreviewInfo[relatedVideos.size()]);
|
||||||
return videoInfo;
|
return videoInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private VideoInfo.AudioStream[] parseDashManifest(String dashManifest, String decryptoinCode) {
|
private VideoInfo.AudioStream[] parseDashManifest(String dashManifest, String decryptoinCode) {
|
||||||
if(!dashManifest.contains("/signature/")) {
|
if(!dashManifest.contains("/signature/")) {
|
||||||
String encryptedSig = matchGroup1("/s/([a-fA-F0-9\\.]+)", dashManifest);
|
String encryptedSig = matchGroup1("/s/([a-fA-F0-9\\.]+)", dashManifest);
|
||||||
@ -391,10 +491,12 @@ public class YoutubeExtractor implements Extractor {
|
|||||||
}
|
}
|
||||||
return audioStreams.toArray(new VideoInfo.AudioStream[audioStreams.size()]);
|
return audioStreams.toArray(new VideoInfo.AudioStream[audioStreams.size()]);
|
||||||
}
|
}
|
||||||
|
/**Provides information about links to other videos on the video page, such as related videos.
|
||||||
private VideoInfoItem extractVideoInfoItem(Element li) {
|
* This is encapsulated in a VideoPreviewInfo object,
|
||||||
VideoInfoItem info = new VideoInfoItem();
|
* which is a subset of the fields in a full VideoInfo.*/
|
||||||
info.webpage_url = li.select("a[class*=\"content-link\"]").first()
|
private VideoPreviewInfo extractVideoPreviewInfo(Element li) {
|
||||||
|
VideoPreviewInfo info = new VideoPreviewInfo();
|
||||||
|
info.webpage_url = li.select("a.content-link").first()
|
||||||
.attr("abs:href");
|
.attr("abs:href");
|
||||||
try {
|
try {
|
||||||
info.id = matchGroup1("v=([0-9a-zA-Z-]*)", info.webpage_url);
|
info.id = matchGroup1("v=([0-9a-zA-Z-]*)", info.webpage_url);
|
||||||
@ -403,14 +505,25 @@ public class YoutubeExtractor implements Extractor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//todo: check NullPointerException causing
|
//todo: check NullPointerException causing
|
||||||
info.title = li.select("span[class=\"title\"]").first().text();
|
info.title = li.select("span.title").first().text();
|
||||||
info.view_count = li.select("span[class*=\"view-count\"]").first().text();
|
//this page causes the NullPointerException, after finding it by searching for "tjvg":
|
||||||
info.uploader = li.select("span[class=\"g-hovercard\"]").first().text();
|
//https://www.youtube.com/watch?v=Uqg0aEhLFAg
|
||||||
info.duration = li.select("span[class=\"video-time\"]").first().text();
|
String views = li.select("span.view-count").first().text();
|
||||||
|
Log.i(TAG, "title:"+info.title);
|
||||||
|
Log.i(TAG, "view count:"+views);
|
||||||
|
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
|
||||||
|
info.view_count = 0;
|
||||||
|
}
|
||||||
|
info.uploader = li.select("span.g-hovercard").first().text();
|
||||||
|
|
||||||
|
info.duration = li.select("span.video-time").first().text();
|
||||||
|
|
||||||
Element img = li.select("img").first();
|
Element img = li.select("img").first();
|
||||||
info.thumbnail_url = img.attr("abs:src");
|
info.thumbnail_url = img.attr("abs:src");
|
||||||
// Sometimes youtube sends links to gif files witch somehow seam to not exist
|
// 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
|
// anymore. Items with such gif also offer a secondary image source. So we are going
|
||||||
// to use that if we caught such an item.
|
// to use that if we caught such an item.
|
||||||
if(info.thumbnail_url.contains(".gif")) {
|
if(info.thumbnail_url.contains(".gif")) {
|
||||||
@ -469,15 +582,19 @@ public class YoutubeExtractor implements Extractor {
|
|||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String cleanUrl(String complexUrl) {
|
||||||
|
return getVideoUrl(getVideoId(complexUrl));
|
||||||
|
}
|
||||||
|
|
||||||
private String matchGroup1(String pattern, String input) {
|
private String matchGroup1(String pattern, String input) {
|
||||||
Pattern pat = Pattern.compile(pattern);
|
Pattern pat = Pattern.compile(pattern);
|
||||||
Matcher mat = pat.matcher(input);
|
Matcher mat = pat.matcher(input);
|
||||||
boolean foundMatch = mat.find();
|
boolean foundMatch = mat.find();
|
||||||
if(foundMatch){
|
if (foundMatch) {
|
||||||
return mat.group(1);
|
return mat.group(1);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Log.e(TAG, "failed to find pattern \""+pattern+"\" inside of \""+input+"\"");
|
Log.w(TAG, "failed to find pattern \""+pattern+"\" inside of \""+input+"\"");
|
||||||
new Exception("failed to find pattern \""+pattern+"\"").printStackTrace();
|
new Exception("failed to find pattern \""+pattern+"\"").printStackTrace();
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package org.schabi.newpipe.youtube;
|
package org.schabi.newpipe.services.youtube;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@ -7,8 +7,8 @@ import org.jsoup.Jsoup;
|
|||||||
import org.jsoup.nodes.Document;
|
import org.jsoup.nodes.Document;
|
||||||
import org.jsoup.nodes.Element;
|
import org.jsoup.nodes.Element;
|
||||||
import org.schabi.newpipe.Downloader;
|
import org.schabi.newpipe.Downloader;
|
||||||
import org.schabi.newpipe.SearchEngine;
|
import org.schabi.newpipe.services.SearchEngine;
|
||||||
import org.schabi.newpipe.VideoInfoItem;
|
import org.schabi.newpipe.VideoPreviewInfo;
|
||||||
import org.w3c.dom.Node;
|
import org.w3c.dom.Node;
|
||||||
import org.w3c.dom.NodeList;
|
import org.w3c.dom.NodeList;
|
||||||
import org.xml.sax.InputSource;
|
import org.xml.sax.InputSource;
|
||||||
@ -62,7 +62,7 @@ public class YoutubeSearchEngine implements SearchEngine {
|
|||||||
String site;
|
String site;
|
||||||
String url = builder.build().toString();
|
String url = builder.build().toString();
|
||||||
//if we've been passed a valid language code, append it to the URL
|
//if we've been passed a valid language code, append it to the URL
|
||||||
if(languageCode.length() > 0) {
|
if(!languageCode.isEmpty()) {
|
||||||
//assert Pattern.matches("[a-z]{2}(-([A-Z]{2}|[0-9]{1,3}))?", languageCode);
|
//assert Pattern.matches("[a-z]{2}(-([A-Z]{2}|[0-9]{1,3}))?", languageCode);
|
||||||
site = Downloader.download(url, languageCode);
|
site = Downloader.download(url, languageCode);
|
||||||
}
|
}
|
||||||
@ -101,7 +101,8 @@ public class YoutubeSearchEngine implements SearchEngine {
|
|||||||
|
|
||||||
// video item type
|
// video item type
|
||||||
} else if(!((el = item.select("div[class*=\"yt-lockup-video\"").first()) == null)) {
|
} else if(!((el = item.select("div[class*=\"yt-lockup-video\"").first()) == null)) {
|
||||||
VideoInfoItem resultItem = new VideoInfoItem();
|
//todo: de-duplicate this with YoutubeExtractor.getVideoPreviewInfo()
|
||||||
|
VideoPreviewInfo resultItem = new VideoPreviewInfo();
|
||||||
Element dl = el.select("h3").first().select("a").first();
|
Element dl = el.select("h3").first().select("a").first();
|
||||||
resultItem.webpage_url = dl.attr("abs:href");
|
resultItem.webpage_url = dl.attr("abs:href");
|
||||||
try {
|
try {
|
||||||
@ -113,8 +114,9 @@ public class YoutubeSearchEngine implements SearchEngine {
|
|||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
resultItem.title = dl.text();
|
resultItem.title = dl.text();
|
||||||
resultItem.duration = item.select("span[class=\"video-time\"]").first()
|
|
||||||
.text();
|
resultItem.duration = item.select("span[class=\"video-time\"]").first().text();
|
||||||
|
|
||||||
resultItem.uploader = item.select("div[class=\"yt-lockup-byline\"]").first()
|
resultItem.uploader = item.select("div[class=\"yt-lockup-byline\"]").first()
|
||||||
.select("a").first()
|
.select("a").first()
|
||||||
.text();
|
.text();
|
||||||
@ -132,7 +134,7 @@ public class YoutubeSearchEngine implements SearchEngine {
|
|||||||
}
|
}
|
||||||
result.resultList.add(resultItem);
|
result.resultList.add(resultItem);
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "GREAT FUCKING ERROR");
|
Log.e(TAG, "unexpected element found:\""+el+"\"");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
@ -1,8 +1,8 @@
|
|||||||
package org.schabi.newpipe.youtube;
|
package org.schabi.newpipe.services.youtube;
|
||||||
|
|
||||||
import org.schabi.newpipe.StreamingService;
|
import org.schabi.newpipe.services.StreamingService;
|
||||||
import org.schabi.newpipe.Extractor;
|
import org.schabi.newpipe.services.Extractor;
|
||||||
import org.schabi.newpipe.SearchEngine;
|
import org.schabi.newpipe.services.SearchEngine;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,8 +33,13 @@ public class YoutubeService implements StreamingService {
|
|||||||
return serviceInfo;
|
return serviceInfo;
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public Extractor getExtractorInstance() {
|
public Extractor getExtractorInstance(String url) {
|
||||||
return new YoutubeExtractor();
|
if(acceptUrl(url)) {
|
||||||
|
return new YoutubeExtractor(url);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new IllegalArgumentException("supplied String is not a valid Youtube URL");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public SearchEngine getSearchEngineInstance() {
|
public SearchEngine getSearchEngineInstance() {
|
@ -23,10 +23,10 @@
|
|||||||
<string name="screenRotation">Rotation</string>
|
<string name="screenRotation">Rotation</string>
|
||||||
<string name="title_activity_settings">Einstellungen</string>
|
<string name="title_activity_settings">Einstellungen</string>
|
||||||
<string name="useExternalPlayerTitle">Externen Player benutzen</string>
|
<string name="useExternalPlayerTitle">Externen Player benutzen</string>
|
||||||
<string name="downloadLocation">Download Verzeichnis</string>
|
<string name="downloadLocation">Downloadverzeichnis</string>
|
||||||
<string name="downloadLocationSummary">Verzeichnis in dem heruntergeladene Videos gespeichert werden.</string>
|
<string name="downloadLocationSummary">Verzeichnis in dem heruntergeladene Videos gespeichert werden.</string>
|
||||||
<string name="downloadLocationDialogTitle">Download Verzeichnis eingeben</string>
|
<string name="downloadLocationDialogTitle">Download Verzeichnis eingeben</string>
|
||||||
<string name="autoPlayThroughIntentTitle">Automatisches abspielen durch Intent</string>
|
<string name="autoPlayThroughIntentTitle">Automatisches Abspielen durch Intent</string>
|
||||||
<string name="autoPlayThroughIntentSummary">Startet ein Video automatisch wenn es von einer anderen App aufgerufen wurde.</string>
|
<string name="autoPlayThroughIntentSummary">Startet ein Video automatisch wenn es von einer anderen App aufgerufen wurde.</string>
|
||||||
<string name="defaultResolutionPreferenceTitle">Standard Auflösung</string>
|
<string name="defaultResolutionPreferenceTitle">Standard Auflösung</string>
|
||||||
<string name="playWithKodiTitle">Mit Kodi abspielen</string>
|
<string name="playWithKodiTitle">Mit Kodi abspielen</string>
|
||||||
@ -46,7 +46,7 @@
|
|||||||
<item>Audio</item>
|
<item>Audio</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string name="nextVideoTitle">Nächstes Video</string>
|
<string name="nextVideoTitle">Nächstes Video</string>
|
||||||
<string name="showNextAndSimilarTitle">Zeige nächstes und änliche Videos</string>
|
<string name="showNextAndSimilarTitle">Zeige nächstes und ähnliche Videos</string>
|
||||||
<string name="urlNotSupportedText">URL wird nicht unterstützt.</string>
|
<string name="urlNotSupportedText">URL wird nicht unterstützt.</string>
|
||||||
<string name="showSimilarVideosButtonText">Ähnliche Videos</string>
|
<string name="showSimilarVideosButtonText">Ähnliche Videos</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -35,4 +35,14 @@
|
|||||||
<string name="uploadDatePrefix">Mise en ligne le </string>
|
<string name="uploadDatePrefix">Mise en ligne le </string>
|
||||||
<string name="useExternalPlayerTitle">Utiliser un lecteur externe</string>
|
<string name="useExternalPlayerTitle">Utiliser un lecteur externe</string>
|
||||||
<string name="viewSufix">vues</string>
|
<string name="viewSufix">vues</string>
|
||||||
</resources>
|
<string name="leftPlayButtonTitle">Afficher le bouton de lecture sur la gauche.</string>
|
||||||
|
<string name="playAudio">Audio</string>
|
||||||
|
<string name="defaultAudioFormatTitle">Format audio par défaut</string>
|
||||||
|
<string name="webMAudioDescription">WebM- format libre</string>
|
||||||
|
<string name="m4aAudioDescription">m4a - meilleur qualité</string>
|
||||||
|
<string name="downloadDialogTitle">Télécharger</string>
|
||||||
|
<string name="nextVideoTitle">Vidéo suivante</string>
|
||||||
|
<string name="showNextAndSimilarTitle">Afficher les vidéos suivantes et similaires</string>
|
||||||
|
<string name="urlNotSupportedText">URL non supportée.</string>
|
||||||
|
<string name="showSimilarVideosButtonText">Vidéos similaires</string>
|
||||||
|
</resources>
|
||||||
|
@ -47,4 +47,5 @@
|
|||||||
<string name="urlNotSupportedText">URL wordt niet ondersteund.</string>
|
<string name="urlNotSupportedText">URL wordt niet ondersteund.</string>
|
||||||
<string name="showSimilarVideosButtonText">Vergelijkbare videos</string>
|
<string name="showSimilarVideosButtonText">Vergelijkbare videos</string>
|
||||||
<string name="showNextAndSimilarTitle">Laat volgende en vergelijkbare videos zien</string>
|
<string name="showNextAndSimilarTitle">Laat volgende en vergelijkbare videos zien</string>
|
||||||
</resources>
|
<string name="searchLanguageTitle">Voorkeurs content taal</string>
|
||||||
|
</resources>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<string name="title_videoitem_detail">Јутјуб цев</string>
|
<string name="title_videoitem_detail">Јутјуб цев</string>
|
||||||
<string name="nothingFound">Ништа није нађено</string>
|
<string name="nothingFound">Ништа није нађено</string>
|
||||||
<string name="viewSufix">приказа</string>
|
<string name="viewSufix">приказа</string>
|
||||||
<string name="uploadDatePrefix">"Отпремљено "</string>
|
<string name="uploadDatePrefix">"Отпремљен "</string>
|
||||||
<string name="noPlayerFound">Нема плејера токова. Можда желите да га инсталирате.</string>
|
<string name="noPlayerFound">Нема плејера токова. Можда желите да га инсталирате.</string>
|
||||||
<string name="installStreamPlayer">Инсталирај</string>
|
<string name="installStreamPlayer">Инсталирај</string>
|
||||||
<string name="cancel">Одустани</string>
|
<string name="cancel">Одустани</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user