1
0
mirror of https://github.com/TeamNewPipe/NewPipe.git synced 2024-11-22 02:53:09 +01:00

Merge pull request #100 from theScrabi/refactor

+ Implemented timestamps
* renamed `VideoInfoItem` to `VideoPreviewInfo`
* Moved streaming service-related classes into their own, new package: "services"
+ Added javadoc to some classes and methods (where functionality is known well enough to explain)
- De-duplicated common fields between `VideoInfo` and `VideoPreviewInfo` by moving them into a common superclass: `AbstractVideoInfo`
- Removed 2 methods in `PlayVideoActivity` which only call `super()`, and therefore are unnecessary: `onResume()` and `onPostCreate(Bundle)`
+ Added `VideoInfo(AbstractVideoInfo)` constructor
    - to support converting `VideoPreviewInfo`s into `VideoInfo`s, to reuse scraped info (yet to be implemented)
* Made the Extractor class behave as a per-video object;
    - most method return values are video-specific, so it makes sense (to me) to have Extractor be stateful. 
    - The only stateless methods are getVideoUrl(), getVideoId() and loadDecryptionCode(String)
* Implemented a constructor for YoutubeExtractor, which performs all initialisation work, such as fetching `Jsoup.Document`, and `playerArgs:JSONObject`
This commit is contained in:
Adam Howard 2015-11-21 11:11:17 +00:00
commit 94293ca9d9
21 changed files with 642 additions and 338 deletions

View 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;
}

View File

@ -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);

View File

@ -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 = "";

View File

@ -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);
}

View File

@ -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;

View File

@ -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();

View File

@ -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;
}
} }
} }

View File

@ -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;

View File

@ -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));

View File

@ -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) {

View File

@ -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);

View File

@ -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();

View File

@ -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;
} }

View File

@ -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];
} }
}; };
} }

View 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();
}

View File

@ -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);

View File

@ -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 = {

View File

@ -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

View File

@ -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 "";
} }

View File

@ -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;

View File

@ -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() {