0
0
mirror of https://github.com/mediathekview/zapp.git synced 2024-09-19 20:02:17 +02:00

Implement and use basic program info module for most channels #5

This commit is contained in:
Christine Emrich 2016-09-24 21:46:26 +02:00
parent ad39e520fd
commit 2a961adda6
32 changed files with 1144 additions and 10 deletions

View File

@ -10,6 +10,7 @@
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/programguide" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />

View File

@ -4,6 +4,7 @@
<modules>
<module fileurl="file://$PROJECT_DIR$/Zapp.iml" filepath="$PROJECT_DIR$/Zapp.iml" />
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
<module fileurl="file://$PROJECT_DIR$/programguide/programguide.iml" filepath="$PROJECT_DIR$/programguide/programguide.iml" />
</modules>
</component>
</project>

View File

@ -26,19 +26,17 @@ android {
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile fileTree(include: ['*.jar'], dir: 'libs')
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:24.2.1'
compile 'com.android.support:support-v4:24.2.1'
compile 'com.android.support:design:24.2.1'
compile 'com.google.code.gson:gson:2.7'
compile 'commons-io:commons-io:2.5'
// changelog
compile 'com.github.porokoro.paperboy:paperboy:3.0.0'
// for butterknive:
compile 'com.jakewharton:butterknife:8.4.0'
apt 'com.jakewharton:butterknife-compiler:8.4.0'
compile project(':programguide')
}

View File

@ -26,6 +26,7 @@ import de.christinecoenen.code.zapp.model.IChannelList;
import de.christinecoenen.code.zapp.model.json.JsonChannelList;
import de.christinecoenen.code.zapp.utils.view.ClickableViewPager;
import de.christinecoenen.code.zapp.utils.view.FullscreenActivity;
import de.christinecoenen.code.zapp.views.ProgrammInfoView;
public class ChannelDetailActivity extends FullscreenActivity {
@ -35,6 +36,7 @@ public class ChannelDetailActivity extends FullscreenActivity {
protected @BindView(R.id.viewpager_channels) ClickableViewPager viewPager;
protected @BindView(R.id.video) VideoView videoView;
protected @BindView(R.id.progressbar_video) ProgressBar progressView;
protected @BindView(R.id.programm_info) ProgrammInfoView programmInfoView;
protected @BindDrawable(android.R.drawable.ic_media_pause) Drawable pauseIcon;
protected @BindDrawable(android.R.drawable.ic_media_play) Drawable playIcon;
@ -92,6 +94,7 @@ public class ChannelDetailActivity extends FullscreenActivity {
currentChannel = channel;
setTitle(channel.getName());
playDelayed();
programmInfoView.setChannel(channel);
}
};

View File

@ -4,10 +4,19 @@ import java.io.Serializable;
public class ChannelModel implements Serializable {
private String id;
private String name;
private String streamUrl;
private int drawableId;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
@ -35,7 +44,8 @@ public class ChannelModel implements Serializable {
@Override
public String toString() {
return "ChannelModel{" +
"name='" + name + '\'' +
"id='" + id + '\'' +
", name='" + name + '\'' +
", streamUrl='" + streamUrl + '\'' +
", drawableId=" + drawableId +
'}';

View File

@ -60,6 +60,7 @@ class JsonChannelsParser {
getIdentifier(jsonModel.logoName, "drawable", context.getPackageName());
ChannelModel model = new ChannelModel();
model.setId(jsonModel.id);
model.setName(jsonModel.name);
model.setStreamUrl(jsonModel.streamUrl);
model.setDrawableId(logoResourceId);

View File

@ -0,0 +1,82 @@
package de.christinecoenen.code.zapp.views;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.widget.LinearLayout;
import android.widget.TextView;
import butterknife.BindView;
import butterknife.ButterKnife;
import de.christinecoenen.code.programguide.ProgramGuideRequest;
import de.christinecoenen.code.programguide.model.Show;
import de.christinecoenen.code.zapp.R;
import de.christinecoenen.code.zapp.model.ChannelModel;
public class ProgrammInfoView extends LinearLayout {
private static final String TAG = ProgrammInfoView.class.getSimpleName();
protected @BindView(R.id.text_show_title) TextView showTitleView;
protected @BindView(R.id.text_show_subtitle) TextView showSubtitleView;
private ProgramGuideRequest currentShowInfoRequest;
private final ProgramGuideRequest.Listener programGuideListener = new ProgramGuideRequest.Listener() {
@Override
public void onRequestError() {
Log.w(TAG, "could not load show info");
showTitleView.setText(R.string.activity_channel_detail_info_error);
showSubtitleView.setText("");
}
@Override
public void onRequestSuccess(Show currentShow) {
Log.w(TAG, "show info loaded: " + currentShow);
showTitleView.setText(currentShow.getTitle());
showSubtitleView.setText(currentShow.getSubtitle());
}
};
public ProgrammInfoView(Context context, AttributeSet attrs) {
super(context, attrs);
setOrientation(LinearLayout.VERTICAL);
setGravity(Gravity.CENTER_VERTICAL);
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.view_programm_info, this, true);
ButterKnife.bind(this, this);
}
public ProgrammInfoView(Context context) {
this(context, null);
}
public void setChannel(ChannelModel channel) {
chancelProgramGuideLoading();
loadProgramGuide(channel.getId());
}
private void loadProgramGuide(String channelId) {
currentShowInfoRequest = new ProgramGuideRequest(getContext())
.setChannelId(channelId)
.setListener(programGuideListener)
.execute();
}
private void chancelProgramGuideLoading() {
showTitleView.setText("");
showSubtitleView.setText("");
if (currentShowInfoRequest != null) {
currentShowInfoRequest.cancel();
}
}
}

View File

@ -49,10 +49,18 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:orientation="horizontal"
android:orientation="vertical"
android:paddingStart="@dimen/activity_horizontal_margin"
android:paddingEnd="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
android:background="@color/black_overlay"
tools:ignore="UselessParent">
<!-- add content here that should show up on click -->
<de.christinecoenen.code.zapp.views.ProgrammInfoView
android:id="@+id/programm_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</FrameLayout>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<TextView
android:id="@+id/text_show_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.AppCompat.Large.Inverse"
tools:text="Show Title"/>
<TextView
android:id="@+id/text_show_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.AppCompat.Medium.Inverse"
tools:text="Show subtitle"/>
</merge>

View File

@ -1,6 +1,6 @@
[
{
"id": "das-erste",
"id": "das_erste",
"name": "Das Erste",
"stream_url": "http://live-lh.daserste.de/i/daserste_de@91204/master.m3u8",
"logo_name": "channel_logo_das_erste"
@ -18,7 +18,7 @@
"logo_name": "channel_logo_arte"
},
{
"id": "3sat",
"id": "dreisat",
"name": "3sat",
"stream_url": "http://zdf0910-lh.akamaihd.net/i/dach10_v1@392872/master.m3u8",
"logo_name": "channel_logo_3sat"

View File

@ -3,6 +3,8 @@
<string name="activity_changelog_title">Changelog</string>
<string name="activity_channel_detail_info_error">Keine Programminfo</string>
<!-- menus -->
<string name="action_share">Teilen</string>

1
programguide/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

38
programguide/build.gradle Normal file
View File

@ -0,0 +1,38 @@
apply plugin: 'com.android.library'
android {
compileSdkVersion 24
buildToolsVersion "24.0.2"
defaultConfig {
minSdkVersion 21
targetSdkVersion 24
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
// avoid warning for "Not all execution paths return a value"
return void
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:24.2.1'
testCompile 'junit:junit:4.12'
compile 'com.android.volley:volley:1.0.0'
compile 'joda-time:joda-time:2.9.4'
compile 'org.jsoup:jsoup:1.9.2'
}

17
programguide/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,17 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in C:\tools\android-sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

View File

@ -0,0 +1,26 @@
package de.christinecoenen.code.programguide;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumentation test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("de.christinecoenen.code.programmguide.test", appContext.getPackageName());
}
}

View File

@ -0,0 +1,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.christinecoenen.code.programguide">
<uses-permission android:name="android.permission.INTERNET"/>
<application android:allowBackup="false"
android:label="@string/app_name"
android:supportsRtl="false">
</application>
</manifest>

View File

@ -0,0 +1,64 @@
package de.christinecoenen.code.programguide;
import android.util.Log;
import java.util.HashMap;
import java.util.Map;
import de.christinecoenen.code.programguide.model.Channel;
import de.christinecoenen.code.programguide.model.Show;
/**
* Cache singleton for currently running show on any channel.
*/
public class Cache {
private static final String TAG = Cache.class.getSimpleName();
private static Cache instance = null;
private final Map<Channel, Show> shows = new HashMap<>();
public static Cache getInstance() {
if (instance == null) {
instance = new Cache();
}
return instance;
}
public void save(Channel channel, Show show) {
this.shows.put(channel, show);
}
public void save(Map<Channel, Show> shows) {
this.shows.putAll(shows);
}
/**
* @return currently running show or null in case of cache miss
*/
public Show getShow(Channel channel) {
Show show = shows.get(channel);
if (show == null) {
Log.d(TAG, "cache miss: " + channel);
return null;
}
if (show.getEndTime() == null) {
Log.d(TAG, "cache miss: " + channel + ", no show end time");
return null;
}
if (show.getEndTime().isBeforeNow()) {
Log.d(TAG, "cache miss: " + channel + ", show too old");
shows.remove(channel);
return null;
}
Log.d(TAG, "cache hit: " + channel + " - " + show);
return show;
}
}

View File

@ -0,0 +1,79 @@
package de.christinecoenen.code.programguide;
import android.content.Context;
import android.util.Log;
import de.christinecoenen.code.programguide.model.Channel;
import de.christinecoenen.code.programguide.model.Show;
import de.christinecoenen.code.programguide.plugins.IProgramGuideDownloader;
import de.christinecoenen.code.programguide.plugins.PluginRegistry;
public class ProgramGuideRequest {
private static final String TAG = ProgramGuideRequest.class.getSimpleName();
private final Context context;
private Listener listener;
private Channel channelId;
private IProgramGuideDownloader downloader;
public ProgramGuideRequest(Context context) {
this.context = context;
}
public ProgramGuideRequest setListener(Listener listener) {
this.listener = listener;
return this;
}
@SuppressWarnings("WeakerAccess")
public ProgramGuideRequest setChannelId(Channel channelId) {
this.channelId = channelId;
return this;
}
public ProgramGuideRequest setChannelId(String channelId) {
Channel newChannel = null;
try {
newChannel = Channel.getById(channelId);
} catch (IllegalArgumentException e) {
Log.w(TAG, channelId + " is no valid channel id");
}
return setChannelId(newChannel);
}
public ProgramGuideRequest execute() {
if (listener == null) {
throw new RuntimeException("listener not set");
}
if (channelId == null) {
Log.w(TAG, "no valid channel id set");
listener.onRequestError();
return this;
}
downloader = PluginRegistry.getInstance(context).getDownloader(channelId);
if (downloader == null) {
listener.onRequestError();
} else {
downloader.download(listener);
}
return this;
}
public void cancel() {
if (downloader != null) {
downloader.cancel();
}
}
public interface Listener {
void onRequestError();
void onRequestSuccess(Show currentShow);
}
}

View File

@ -0,0 +1,50 @@
package de.christinecoenen.code.programguide.model;
public enum Channel {
DAS_ERSTE("das_erste"),
BR_NORD("br_nord"),
BR_SUED("br_sued"),
HR("hr"),
MDR_SACHSEN("mdr_sachsen"),
MDR_SACHSEN_ANHALT("mdr_sachsen_anhalt"),
MDR_THUERINGEN("mdr_thueringen"),
NDR_HAMBURG("ndr_hh"),
NDR_MECKLENBURG_VORPOMMERN("ndr_mv"),
NDR_NIEDERSACHSEN("ndr_nds"),
NDR_SCHLESWIG_HOLSTEIN("ndr_sh"),
RBB_BERLIN("rbb_berlin"),
RBB_BRANDENBURG("rbb_brandenburg"),
SR("sr"),
SWR_BADEN_WUERTTEMBERG("swr_bw"),
SWR_RHEINLAND_PFALZ("swr_rp"),
WDR("wdr"),
ARD_ALPHA("ard_alpha"),
TAGESSCHAU24("tagesschau24"),
ONE("one"),
ARTE("arte"),
ZDF("zdf"),
DREISAT("dreisat"),
KIKA("kika"),
PHOENIX("phoenix"),
ZDF_KULTUR("zdf_kultur"),
ZDF_INFO("zdf_info"),
ZDF_NEO("zdf_neo");
public static Channel getById(String id) {
for (Channel channel : Channel.values()) {
if (channel.id.equals(id)) {
return channel;
}
}
throw new IllegalArgumentException();
}
private final String id;
Channel(String id) {
this.id = id;
}
}

View File

@ -0,0 +1,59 @@
package de.christinecoenen.code.programguide.model;
import org.joda.time.DateTime;
public class Show {
private String title;
private String subtitle;
private String description;
private DateTime startTime;
private DateTime endTime;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getSubtitle() {
return subtitle;
}
public void setSubtitle(String subtitle) {
this.subtitle = subtitle;
}
public void setDescription(String description) {
this.description = description;
}
public DateTime getStartTime() {
return startTime;
}
public void setStartTime(DateTime startTime) {
this.startTime = startTime;
}
public DateTime getEndTime() {
return endTime;
}
public void setEndTime(DateTime endTime) {
this.endTime = endTime;
}
@Override
public String toString() {
return "Show{" +
"title='" + title + '\'' +
", subtitle='" + subtitle + '\'' +
", description='" + description + '\'' +
", startTime=" + startTime +
", endTime=" + endTime +
'}';
}
}

View File

@ -0,0 +1,74 @@
package de.christinecoenen.code.programguide.plugins;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import java.util.Map;
import de.christinecoenen.code.programguide.Cache;
import de.christinecoenen.code.programguide.model.Channel;
import de.christinecoenen.code.programguide.ProgramGuideRequest;
import de.christinecoenen.code.programguide.model.Show;
public abstract class BaseProgramGuideDownloader implements IProgramGuideDownloader {
protected final DownloaderListener downloaderListener = new DownloaderListener() {
@Override
public void onRequestError() {
listener.onRequestError();
}
@Override
public void onRequestSuccess(Map<Channel, Show> shows) {
onRequestSuccess(shows.get(channel));
Cache.getInstance().save(shows);
}
@Override
public void onRequestSuccess(Show show) {
listener.onRequestSuccess(show);
Cache.getInstance().save(channel, show);
}
};
protected final RequestQueue queue;
protected Request<?> request;
private final Channel channel;
private ProgramGuideRequest.Listener listener;
protected BaseProgramGuideDownloader(RequestQueue queue, Channel channel) {
this.queue = queue;
this.channel = channel;
}
@Override
public void download(ProgramGuideRequest.Listener listener) {
this.listener = listener;
Show show = Cache.getInstance().getShow(this.channel);
if (show == null) {
download();
} else {
listener.onRequestSuccess(show);
}
}
@Override
public void cancel() {
if (request != null) {
request.cancel();
}
}
protected abstract void download();
@SuppressWarnings("unused")
protected interface DownloaderListener {
void onRequestError();
void onRequestSuccess(Map<Channel, Show> shows);
void onRequestSuccess(Show shows);
}
}

View File

@ -0,0 +1,9 @@
package de.christinecoenen.code.programguide.plugins;
import de.christinecoenen.code.programguide.ProgramGuideRequest;
public interface IProgramGuideDownloader {
void download(ProgramGuideRequest.Listener listener);
void cancel();
}

View File

@ -0,0 +1,50 @@
package de.christinecoenen.code.programguide.plugins;
import android.content.Context;
import com.android.volley.RequestQueue;
import com.android.volley.toolbox.Volley;
import java.util.HashMap;
import java.util.Map;
import de.christinecoenen.code.programguide.model.Channel;
import de.christinecoenen.code.programguide.plugins.ard.ArdDownloader;
import de.christinecoenen.code.programguide.plugins.arte.ArteDownloader;
import de.christinecoenen.code.programguide.plugins.zdf.ZdfDownloader;
public class PluginRegistry {
private static PluginRegistry instance = null;
public static PluginRegistry getInstance(Context context) {
if (instance == null) {
instance = new PluginRegistry(context);
}
return instance;
}
private final Map<Channel, IProgramGuideDownloader> downloaders;
private PluginRegistry(Context context) {
RequestQueue queue = Volley.newRequestQueue(context);
downloaders = new HashMap<>(Channel.values().length);
for (Channel ardChannel : ArdDownloader.CHANNELS) {
downloaders.put(ardChannel, new ArdDownloader(queue, ardChannel));
}
for (Channel zdfChannel : ZdfDownloader.CHANNELS) {
downloaders.put(zdfChannel, new ZdfDownloader(queue, zdfChannel));
}
for (Channel arteChannel : ArteDownloader.CHANNELS) {
downloaders.put(arteChannel, new ArteDownloader(queue, arteChannel));
}
}
public IProgramGuideDownloader getDownloader(Channel channel) {
return downloaders.get(channel);
}
}

View File

@ -0,0 +1,73 @@
package de.christinecoenen.code.programguide.plugins.ard;
import android.util.Log;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.StringRequest;
import java.util.Map;
import de.christinecoenen.code.programguide.model.Channel;
import de.christinecoenen.code.programguide.model.Show;
import de.christinecoenen.code.programguide.plugins.BaseProgramGuideDownloader;
public class ArdDownloader extends BaseProgramGuideDownloader {
public static final Channel[] CHANNELS = new Channel[] {
Channel.DAS_ERSTE,
Channel.BR_NORD,
Channel.BR_SUED,
Channel.HR,
Channel.MDR_SACHSEN,
Channel.MDR_SACHSEN_ANHALT,
Channel.MDR_THUERINGEN,
Channel.NDR_HAMBURG,
Channel.NDR_MECKLENBURG_VORPOMMERN,
Channel.NDR_NIEDERSACHSEN,
Channel.NDR_SCHLESWIG_HOLSTEIN,
Channel.RBB_BERLIN,
Channel.RBB_BRANDENBURG,
Channel.SR,
Channel.SWR_BADEN_WUERTTEMBERG,
Channel.SWR_RHEINLAND_PFALZ,
Channel.WDR,
Channel.ARD_ALPHA,
Channel.TAGESSCHAU24,
Channel.ONE
};
private static final String TAG = ArdDownloader.class.getSimpleName();
private static final String HTML_URL = "http://programm.ard.de/TV/Programm/Load/NavJetztImTV35";
public ArdDownloader(RequestQueue queue, Channel channel) {
super(queue, channel);
}
@Override
public void download() {
request = new StringRequest(HTML_URL,
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
Log.d(TAG, "html loaded: " + response);
parse(response);
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.d(TAG, "error loading html: " + error.getMessage());
downloaderListener.onRequestError();
}
});
queue.add(request);
}
private void parse(String html) {
Map<Channel, Show> shows = ArdParser.parse(html);
downloaderListener.onRequestSuccess(shows);
}
}

View File

@ -0,0 +1,171 @@
package de.christinecoenen.code.programguide.plugins.ard;
import android.util.ArrayMap;
import android.util.Log;
import org.joda.time.DateTime;
import org.joda.time.MutableDateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import de.christinecoenen.code.programguide.model.Channel;
import de.christinecoenen.code.programguide.model.Show;
class ArdParser {
private static final String TAG = ArdParser.class.getSimpleName();
// 20:15
private static final DateTimeFormatter TIME_FORMATTER =
DateTimeFormat.forPattern("HH:mm");
private static final Pattern TIME_PATTERN = Pattern.compile("\\d+:\\d+");
static Map<Channel, Show> parse(String html) {
Map<Channel, Show> shows = new ArrayMap<>();
Document document = Jsoup.parseBodyFragment(html);
Elements items = document.select("li a");
for (Element item : items) {
shows.putAll(parseItem(item));
}
return shows;
}
private static Map<Channel, Show> parseItem(Element item) {
String title = item.select("b").text();
String rawMetadata = item.select("span.small").text();
Show show = parseShow(title, rawMetadata);
List<Channel> channels = getChannels(rawMetadata);
Map<Channel, Show> shows = new ArrayMap<>();
for (Channel channel : channels) {
shows.put(channel, show);
}
return shows;
}
private static Show parseShow(String title, String rawMetadata) {
Show show = new Show();
show.setTitle(title);
setTimes(show, rawMetadata);
Log.d(TAG, show.toString());
return show;
}
private static void setTimes(Show show, String rawMetadata) {
String[] splitMetadata = rawMetadata.split("\\|"); // MDR Fernsehen | 19:30 - 19:50 Uhr
String rawTime = splitMetadata[splitMetadata.length - 1]; // 19:30 - 19:50 Uhr
Matcher matcher = TIME_PATTERN.matcher(rawTime);
if (matcher.find()) {
DateTime startTime = TIME_FORMATTER.parseDateTime(matcher.group());
MutableDateTime startTimeDate = MutableDateTime.now();
startTimeDate.setMillisOfDay(0);
startTimeDate.setMinuteOfDay(startTime.getMinuteOfDay());
show.setStartTime(startTimeDate.toDateTime());
}
if (matcher.find()) {
DateTime endTime = TIME_FORMATTER.parseDateTime(matcher.group());
MutableDateTime endTimeDate = MutableDateTime.now();
endTimeDate.setMillisOfDay(0);
endTimeDate.setMinuteOfDay(endTime.getMinuteOfDay());
show.setEndTime(endTimeDate.toDateTime());
if (show.getEndTime().isBefore(show.getStartTime())) {
// show plays around midnight
if (DateTime.now().getHourOfDay() < 12) {
// noew is morning - startTime was yesterday
DateTime newStartTime = show.getStartTime().minusDays(1);
show.setStartTime(newStartTime);
} else {
// now is evening - end time is tomorrow
DateTime newEndTime = show.getEndTime().plusDays(1);
show.setEndTime(newEndTime);
}
}
}
}
private static List<Channel> getChannels(String rawMetadata) {
// SR Fernsehen | SWR Ferns. BW | SWR Ferns. RP | 20:15 - 21:00 Uhr
String[] splitMetadata = rawMetadata.split("\\|");
List<Channel> channels = new ArrayList<>();
for (int i = 0; i < splitMetadata.length-1; i++) {
Channel[] parsedChannels = getParsedChannels(splitMetadata[i]);
if (parsedChannels != null) {
channels.addAll(Arrays.asList(parsedChannels));
}
}
return channels;
}
private static Channel[] getParsedChannels(String htmlName) {
switch(htmlName.trim()) {
case "ARD-alpha":
return new Channel[] { Channel.ARD_ALPHA };
case "Das Erste":
return new Channel[] { Channel.DAS_ERSTE };
case "SR Fernsehen":
return new Channel[] { Channel.SR };
case "tagesschau24":
return new Channel[] { Channel.TAGESSCHAU24 };
case "SWR Ferns. BW":
return new Channel[] { Channel.SWR_BADEN_WUERTTEMBERG };
case "SWR Ferns. RP":
return new Channel[] { Channel.SWR_RHEINLAND_PFALZ };
case "WDR Fernsehen":
return new Channel[] { Channel.WDR };
case "ONE":
return new Channel[] { Channel.ONE };
case "hr-fernsehen":
return new Channel[] { Channel.HR };
case "BR Fernsehen":
return new Channel[] {
Channel.BR_NORD,
Channel.BR_SUED
};
case "MDR Fernsehen":
return new Channel[] {
Channel.MDR_SACHSEN,
Channel.MDR_SACHSEN_ANHALT,
Channel.MDR_THUERINGEN
};
case "NDR Fernsehen":
return new Channel[] {
Channel.NDR_HAMBURG,
Channel.NDR_MECKLENBURG_VORPOMMERN,
Channel.NDR_NIEDERSACHSEN,
Channel.NDR_SCHLESWIG_HOLSTEIN
};
case "rbb Fernsehen":
return new Channel[] {
Channel.RBB_BERLIN,
Channel.RBB_BRANDENBURG
};
case "EinsPlus": // ?
default:
return null;
}
}
}

View File

@ -0,0 +1,65 @@
package de.christinecoenen.code.programguide.plugins.arte;
import android.util.Log;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.JsonObjectRequest;
import org.json.JSONException;
import org.json.JSONObject;
import de.christinecoenen.code.programguide.model.Channel;
import de.christinecoenen.code.programguide.model.Show;
import de.christinecoenen.code.programguide.plugins.BaseProgramGuideDownloader;
public class ArteDownloader extends BaseProgramGuideDownloader {
public static final Channel[] CHANNELS = new Channel[] {
Channel.ARTE
};
private static final String TAG = ArteDownloader.class.getSimpleName();
private static final String JSON_URL = "https://api.arte.tv/api/player/v1/livestream/de";
public ArteDownloader(RequestQueue queue, Channel channel) {
super(queue, channel);
}
@Override
public void download() {
request = new JsonObjectRequest(Request.Method.GET, JSON_URL, null,
new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
Log.d(TAG, "json loaded: " + response.toString());
parse(response);
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.d(TAG, "error loading json: " + error.getMessage());
downloaderListener.onRequestError();
}
});
queue.add(request);
}
private void parse(JSONObject json) {
Show show;
try {
show = ArteParser.parse(json);
} catch (JSONException e) {
Log.d(TAG, e.getMessage());
downloaderListener.onRequestError();
return;
}
downloaderListener.onRequestSuccess(show);
}
}

View File

@ -0,0 +1,42 @@
package de.christinecoenen.code.programguide.plugins.arte;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import de.christinecoenen.code.programguide.model.Show;
/**
* Parses ARTE program info json.
* @see "https://api.arte.tv/api/player/v1/livestream/de"
*/
class ArteParser {
private static final String TAG = ArteParser.class.getSimpleName();
static Show parse(JSONObject json) throws JSONException {
Show show = new Show();
JSONObject root = json.getJSONObject("videoJsonPlayer");
String title = root.getString("VTI");
String subtitle = getOptionalString(root, "subtitle");
String description = getOptionalString(root, "V7T");
show.setTitle(title);
show.setSubtitle(subtitle);
show.setDescription(description);
return show;
}
private static String getOptionalString(JSONObject json, String key) {
try {
return json.getString(key);
} catch (JSONException e) {
Log.d(TAG, "optional json key is not present: " + key);
}
return null;
}
}

View File

@ -0,0 +1,74 @@
package de.christinecoenen.code.programguide.plugins.zdf;
import android.util.Log;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.JsonObjectRequest;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Map;
import de.christinecoenen.code.programguide.model.Channel;
import de.christinecoenen.code.programguide.model.Show;
import de.christinecoenen.code.programguide.plugins.BaseProgramGuideDownloader;
public class ZdfDownloader extends BaseProgramGuideDownloader {
public static final Channel[] CHANNELS = new Channel[] {
Channel.ZDF,
Channel.KIKA,
Channel.PHOENIX,
Channel.ZDF_INFO,
Channel.ZDF_KULTUR,
Channel.ZDF_NEO,
Channel.DREISAT
};
private static final String TAG = ZdfDownloader.class.getSimpleName();
private static final String URL_ZDF_ALL = "http://sofa01.zdf.de/epgservice/v2/all/now/json";
public ZdfDownloader(RequestQueue queue, Channel channel) {
super(queue, channel);
}
@Override
public void download() {
request = new JsonObjectRequest(Request.Method.GET, URL_ZDF_ALL, null,
new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
Log.d(TAG, "json loaded: " + response.toString());
parse(response);
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.d(TAG, "error loading json: " + error.getMessage());
downloaderListener.onRequestError();
}
});
queue.add(request);
}
private void parse(JSONObject json) {
Map<Channel, Show> shows;
try {
shows = ZdfParser.parse(json);
} catch (JSONException e) {
Log.d(TAG, e.getMessage());
downloaderListener.onRequestError();
return;
}
downloaderListener.onRequestSuccess(shows);
}
}

View File

@ -0,0 +1,84 @@
package de.christinecoenen.code.programguide.plugins.zdf;
import android.util.ArrayMap;
import android.util.Log;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Map;
import de.christinecoenen.code.programguide.model.Channel;
import de.christinecoenen.code.programguide.model.Show;
/**
* Parses program info json of all ZDF related channels.
* @see "http://sofa01.zdf.de/epgservice/v2/all/now/json"
*/
class ZdfParser {
private static final String TAG = ZdfParser.class.getSimpleName();
// 2016-09-22T16:10:00+02:00
private static final DateTimeFormatter formatter =
DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ssZ");
static Map<Channel, Show> parse(JSONObject json) throws JSONException {
JSONObject responseJson = json.getJSONObject("response");
JSONObject channelsJson = responseJson.getJSONObject("sender");
Map<Channel, Show> shows = new ArrayMap<>();
shows.put(Channel.ZDF, getChannelShow("ZDF", channelsJson));
shows.put(Channel.KIKA, getChannelShow("KI.KA", channelsJson));
shows.put(Channel.ZDF_INFO, getChannelShow("ZDFinfo", channelsJson));
shows.put(Channel.ZDF_KULTUR, getChannelShow("ZDF.kultur", channelsJson));
shows.put(Channel.ZDF_NEO, getChannelShow("ZDFneo", channelsJson));
shows.put(Channel.PHOENIX, getChannelShow("phoenix", channelsJson));
shows.put(Channel.DREISAT, getChannelShow("dreisat", channelsJson));
return shows;
}
private static Show getChannelShow(String channelKey, JSONObject channelsJson) throws JSONException {
JSONObject channelJson = channelsJson.getJSONObject(channelKey);
JSONArray showsJson = channelJson.getJSONArray("sendungen");
JSONObject showMetadataJson = ((JSONObject) showsJson.get(0)).getJSONObject("sendung");
return parseShow(showMetadataJson);
}
private static Show parseShow(JSONObject showMetadata) throws JSONException {
Show show = new Show();
JSONObject showJson = showMetadata.getJSONObject("value");
String title = showJson.getString("titel");
String subtitle = getOptionalString(showJson, "untertitel");
String description = getOptionalString(showJson, "beschreibung");
DateTime startTime = formatter.parseDateTime(showJson.getString("time"));
DateTime endTime = formatter.parseDateTime(showJson.getString("endTime"));
show.setTitle(title);
show.setSubtitle(subtitle);
show.setDescription(description);
show.setStartTime(startTime);
show.setEndTime(endTime);
return show;
}
private static String getOptionalString(JSONObject json, String key) {
try {
return json.getString(key);
} catch (JSONException e) {
Log.d(TAG, "optional json key is not present: " + key);
}
return null;
}
}

View File

@ -0,0 +1,3 @@
<resources>
<string name="app_name">Program Guide</string>
</resources>

View File

@ -0,0 +1,17 @@
package de.christinecoenen.code.programguide;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}

View File

@ -1 +1 @@
include ':app'
include ':app', ':programguide'