Podcast/js/services.js
2013-04-07 10:12:25 +02:00

870 lines
No EOL
33 KiB
JavaScript

'use strict';
/* Services */
angular.module('podcasts.services', [])
.service('downloaderBackend', ['$http', '$q', 'xmlParser', function($http, $q, xmlParser) {
return {
downloadFile: function(url) {
var deferred = $q.defer();
$http.get(url, {'responseType': 'blob'})
.success(function(file) {
deferred.resolve(file);
})
.error(function() {
deferred.reject();
})
;
return deferred.promise;
},
downloadXml: function(url) {
var deferred = $q.defer();
$http.get(url)
.success(function(xml) {
deferred.resolve(xmlParser.parse(xml));
})
.error(function(data, status, headers, config) {
deferred.reject();
});
return deferred.promise;
}
}
}])
.value('queueList', {
queue: [],
scope: null,
init: function(scope) {
this.scope = scope;
scope.queue = this.queue = [];
},
addToQueue: function(item) {
this.queue.push(item);
if (typeof this.scope !== "undefined") {
this.scope.$apply();
}
}
})
.service('feedItems', ['dbNew', 'db', '$q', function(db, oldDb, $q) {
return {
get: function(id, onSuccess, onFailure) {
db.getOne("feedItem", id)
.then(function(value) {
onSuccess(value);
}, function() {
onFailure();
});
},
getFeedItemFromXml: function(xml) {
var newFeedItem = {},
searchableXml = angular.element(xml);
newFeedItem.guid = searchableXml.find('guid').text();
newFeedItem.title = searchableXml.find('title').text();
newFeedItem.link = searchableXml.find('link').text();
newFeedItem.date = Date.parse(searchableXml.find('pubDate').text());
newFeedItem.description = searchableXml.find('description').text();
newFeedItem.audioUrl = searchableXml.find('enclosure').attr('url');
return newFeedItem;
},
delete: function(id) {
oldDb.delete("feedItem", id);
},
deleteByFeedId: function(feedId) {
oldDb.getCursor("feedItem", function(ixDbCursorReq) {
if (typeof ixDbCursorReq !== "undefined") {
ixDbCursorReq.onsuccess = function(e) {
var cursor = ixDbCursorReq.result || e.result;
if (cursor) {
this.delete(cursor.value.id);
}
}
}
}, null, IDBKeyRange.only(feedId), null, 'ixFeedId');
},
add: function(object) {
var deferred = $q.defer(),
newFeedItem = {
guid: object.guid,
feedId: object.feedId,
title: object.title,
link: object.link,
date: object.date,
description: object.description,
audioUrl: object.audioUrl,
queued: object.queued
};
oldDb.put("feedItem", newFeedItem, undefined, function() {
deferred.resolve(newFeedItem);
});
return deferred.promise;
},
getNextInQueue: function(feedItem) {
var tempQueueList = { queue: [], addToQueue: function(item) { this.queue.push(item); } };
var nextFeedItem = null;
this.listQueue(tempQueueList, function() {
var returnNextValue = false;
angular.forEach(tempQueueList.queue, function(key, value) {
if (returnNextValue) {
nextFeedItem = value;
returnNextValue = false;
}
if (feedItem.id === value.id) {
returnNextValue = true;
}
});
});
return nextFeedItem;
},
listQueue: function(queueList, done) {
oldDb.getCursor("feedItem", function(ixDbCursorReq)
{
if(typeof ixDbCursorReq !== "undefined") {
ixDbCursorReq.onsuccess = function (e) {
var cursor = ixDbCursorReq.result || e.result;
if (cursor) {
queueList.addToQueue(cursor.value);
cursor.continue();
} else {
if (typeof done === 'function') {
done();
}
}
}
}
}, undefined, IDBKeyRange.only(1), undefined, 'ixQueued');
}
}
}])
.service('feeds', ['$log', '$q', 'dbNew', 'db', 'downloaderBackend', 'xmlParser', 'feedItems', function($log, $q, db, dbOld, downloaderBackend, xmlParser, feedItems) {
return {
feeds: [],
add: function(url) {
var feedService = this;
var finishSave = function(newFeed) {
dbOld.put("feed", newFeed, undefined, function(key) {
newFeed.id = key;
feedService.feeds.push(newFeed);
feedService.downloadItems(newFeed);
});
};
// TODO: verify URL format somewhere
var promise = downloaderBackend.downloadXml(url);
promise.then(function(xml) {
var channelChildren = xml.find('channel').children(),
newFeed = {},
imageUrl;
angular.forEach(channelChildren, function(value, key) {
if ('itunes:image' === angular.element(value)[0].nodeName.toLowerCase()) {
imageUrl = angular.element(value).attr('href');
}
if ('itunes:author' === angular.element(value)[0].nodeName.toLowerCase()) {
newFeed.author = angular.element(value).text();
}
});
newFeed.url = url;
newFeed.title = channelChildren.find('title').text();
newFeed.summary = channelChildren.find('description').text();
newFeed.nrQueueItems = 1;
var file = downloaderBackend.downloadFile(imageUrl);
file.then(function(fileBlob) {
newFeed.image = fileBlob;
finishSave(newFeed);
}, function() {
finishSave(newFeed);
});
}, function() {
console.warn('Could not fetch XML for feed, adding just URL for now');
var newFeed = {};
newFeed.url = url;
finishSave(newFeed);
});
},
get: function(id) {
id = parseInt(id, 10);
var deferred = $q.defer();
db.getOne("feed", id)
.then(function(feed) {
if (typeof feed.image === 'string') {
feed.image = new Blob([feed.image]);
}
dbOld.getCursor("feedItem", function(ixDbCursorReq)
{
feed.items = [];
if(typeof ixDbCursorReq !== "undefined") {
ixDbCursorReq.onsuccess = function (e) {
var cursor = ixDbCursorReq.result || e.result;
if (cursor) {
feed.items.push(cursor.value);
cursor.continue();
} else {
deferred.resolve(feed);
}
}
}
}, null, IDBKeyRange.only(feed.id), undefined, 'ixFeedId');
}, function(value) {
deferred.reject();
});
return deferred.promise;
},
list: function($scope) {
var feeds = this.feeds;
dbOld.getCursor("feed", function(ixDbCursorReq)
{
if(typeof ixDbCursorReq !== "undefined") {
ixDbCursorReq.onsuccess = function (e) {
var cursor = ixDbCursorReq.result || e.result;
if (cursor) {
if (typeof cursor.value.image === 'string') {
cursor.value.image = new Blob(
[cursor.value.image],
{type: 'application/octet-stream'}
);
}
feeds.push(cursor.value);
$scope.$apply();
cursor.continue();
}
}
}
});
},
delete: function(id) {
$log.info('Deleting feed with ID ' + id);
feedItems.deleteByFeedId(id);
dbOld.delete("feed", id);
},
/**
*
* @param feedItems
* @param updateStatus function that gets called for each item it goes through
* Takes the feedItem as the argument
*/
downloadAllItems: function(feedItems, updateStatus) {
var feedService = this;
dbOld.getCursor("feed", function(ixDbCursorReq)
{
if(typeof ixDbCursorReq !== "undefined") {
ixDbCursorReq.onsuccess = function (e) {
var cursor = ixDbCursorReq.result || e.result;
if (cursor) {
feedService.downloadItems(cursor.value, updateStatus);
cursor.continue();
} else {
updateStatus();
}
}
}
});
},
downloadItems: function(feedItem, updateStatus) {
var promise = downloaderBackend.downloadXml(feedItem.url),
feedObjects = [];
promise.then(function(data) {
angular.forEach(
data.find('item'),
function(element, index) {
if (index < 3) { // TODO: this should be a global setting and/or a per-feed setting
feedObjects.push(feedItems.getFeedItemFromXml(element))
}
}
);
angular.forEach(feedObjects, function(feedObject, index) {
feedObject.feedId = feedItem.id;
if (0 === index) {
feedObject.queued = 1;
} else {
feedObject.queued = 0;
}
feedItems.add(feedObject)
.then(function(item) {
if (typeof updateStatus === 'function') {
updateStatus(item, feedItem);
}
});
});
});
}
}
}])
.service('xmlParser', ['$window', function($window) {
return {
parse: function(data) {
return angular.element(new $window.DOMParser().parseFromString(data, "text/xml"));
}
};
}])
.service('url', ['$window', function($window) {
return {
url: $window.URL || $window.webkitURL,
createObjectUrl: function(data) {
return this.url.createObjectUrl(data);
}
};
}])
.service('player', ['db', 'url', '$timeout', function(db, url, $timeout) {
return {
db: db,
audio: new Audio(),
feedItem: null,
nowPlaying: {position: 0, duration: 0, title: '', description: '', feed: '', date: 0},
play: function (feedItem, $scope) {
if (feedItem) {
this.feedItem = feedItem;
var audioSrc;
if (feedItem.audio) {
audioSrc = url.createObjectUrl(feedItem.audio);
} else {
audioSrc = feedItem.audioUrl;
}
this.audio.src = audioSrc;
this.updateSong(feedItem, $scope);
if (feedItem.position) {
angular.element(this.audio).bind('canplay', function(event) {
this.currentTime = feedItem.position;
});
}
}
this.audio.play();
var db = this.db;
angular.element(this.audio).bind('pause', function(event) {
feedItem.position = Math.floor(event.target.currentTime);
db.put("feedItem", feedItem);
});
// TODO: add something here for when audio is done to remove from queue and go to next song
angular.element(this.audio).bind('ended', function(event) {
feedItem.queued = 0;
feedItem.position = 0;
db.put("feedItem", feedItem);
// start next item
// get next item from queue
play(nextFeedItem, $scope);
});
},
pause: function() {
this.audio[0].pause();
},
playing: function() {
return !this.audio[0].paused;
},
updateSong: function(feedItem, $scope) {
this.nowPlaying.title = feedItem.title;
var audio = this.audio,
player = this;
$timeout(function() {
player.nowPlaying.duration = audio.duration;
}, 100);
this.nowPlaying.feedItem = feedItem;
this.nowPlaying.description = feedItem.description;
this.nowPlaying.feed = feedItem.feed;
this.nowPlaying.date = feedItem.date;
this.updatePosition($scope);
},
updatePosition: function($scope) {
var audio = this.audio,
player = this;
setInterval(function() {
player.nowPlaying.position = audio.currentTime;
$scope.$apply();
}, 1000);
}
}
}])
.service('pageSwitcher', ['$location', '$route', function($location, $route) {
return {
//TODO: change these getElementById's to something else
pageSwitcher: document.getElementById('pageSwitcher'),
pages: ['queue', 'settings', 'feeds'],
$route: $route,
currentPage: null,
backPage: null,
change: function(current) {
this.currentPage = current;
var nextPage = this.getNextPage(this.currentPage);
angular.element(document.getElementById('pageswitch-icon-' + this.currentPage))
.addClass('next').removeClass('next1 next2');
angular.element(document.getElementById('pageswitch-icon-' + nextPage))
.addClass('next1').removeClass('next next2');
angular.element(document.getElementById('pageswitch-icon-' + this.getNextPage(nextPage)))
.addClass('next2').removeClass('next next1');
},
setBack: function(backPage) {
this.backPage = backPage;
},
getNextPage: function(current) {
var nextPage,
pages = this.pages,
validRoute = false;
angular.forEach(pages, function(value, key) {
if (current === value) {
var nextKey = key + 1;
if (pages[nextKey]) {
nextPage = pages[nextKey];
} else {
nextPage = pages[0];
}
}
});
angular.forEach(this.$route.routes, function(value, key) {
if (key === '/' + nextPage) {
validRoute = true;
}
});
if (!validRoute) {
console.error('no valid route found for pageSwitcher: ' + nextPage);
}
return nextPage;
},
goToPage: function(page) {
if (!page) {
if (this.backPage) {
page = this.backPage;
} else {
page = this.getNextPage(this.currentPage);
}
}
if (this.backPage) {
this.backPage = null;
}
$location.path('/'+page);
}
}
}])
.service('downloader', ['db', 'url', '$http', 'settings', function(db, url, $http, settings) {
return {
allowedToDownload: function(callback) {
settings.get('downloadOnWifi', function(setting) {
if (setting.value) {
//TODO: check if we're on wifi
callback(false);
} else {
callback(true);
}
}, function() {
callback(true); // Default value is "allowed" - maybe change this?
});
},
downloadAll: function(silent) {
var downloader = this;
this.allowedToDownload(function(value) {
if (!value) {
if (typeof silent !== 'undefined' && !silent) {
alert('not Downloading because not on WiFi'); //TODO: nicer error message?
}
} else {
var itemsToDownload = [];
db.getCursor("feedItem", function(ixDbCursorReq)
{
if(typeof ixDbCursorReq !== "undefined") {
ixDbCursorReq.onsuccess = function (e) {
var cursor = ixDbCursorReq.result || e.result;
if (cursor) {
if (!cursor.value.audio && cursor.value.audioUrl) {
itemsToDownload.push(cursor.value);
}
cursor.continue();
} else {
downloader.downloadFiles(itemsToDownload);
}
}
}
}, undefined, IDBKeyRange.only(1), undefined, 'ixQueued');
}
});
},
downloadFiles: function(itemsToDownload) {
var item = itemsToDownload.shift(),
downloader = this;
if (!item) {
return;
}
$http.get(item.audioUrl, {'responseType': 'blob'})
.success(function(data) {
item.audio = data;
item.duration = this.getAudioLength(data);
db.put("feedItem", item);
downloader.downloadFiles(itemsToDownload);
})
;
},
getAudioLength: function(audio) {
var tmpAudio = new Audio();
tmpAudio.autoplay = false;
tmpAudio.muted = true;
tmpAudio.src = url.createObjectURL(audio);
return tmpAudio.duration;
}
};
}])
.filter('time', function() {
return function(input, skip) {
var seconds, minutes, hours;
seconds = Math.floor(input);
if (seconds > 120) {
minutes = Math.floor(seconds/60);
seconds = seconds % 60;
if (seconds < 10) {
seconds = '0' + seconds;
}
}
if (minutes > 60) {
hours = Math.floor(minutes/60);
minutes = minutes % 60;
if (minutes < 10) {
minutes = '0' + minutes;
}
}
if (hours) {
return hours + ':' + minutes + ':' + seconds;
} else if (minutes) {
return minutes + ':' + seconds;
} else {
if (skip) {
return seconds;
}
return seconds + 's';
}
}
})
.filter('timeAgo', function() {
return function(timestamp) {
var diff = ((new Date().getTime()) - (new Date(timestamp).getTime())) / 1000,
day_diff = Math.floor(diff / 86400);
return day_diff == 0 && (
diff < 60 && "just now" ||
diff < 120 && "1 minute ago" ||
diff < 3600 && Math.floor( diff / 60 ) + " minutes ago" ||
diff < 7200 && "1 hour ago" ||
diff < 86400 && Math.floor( diff / 3600 ) + " hours ago") ||
day_diff == 1 && "Yesterday" ||
day_diff < 7 && day_diff + " days ago" ||
day_diff < 31 && Math.ceil( day_diff / 7 ) + " weeks ago" ||
"older than a month";
}
});
angular.module('podcasts.database', [])
.run(function() {
var dbConfig = (function () {
//Create IndexedDB ObjectStore and Indexes via ixDbEz
ixDbEz.createObjStore("feed", "id", true);
ixDbEz.createIndex("feed", "ixUrl", "url", true);
ixDbEz.createObjStore("feedItem", "id", true);
ixDbEz.createIndex("feedItem", "ixGuid", "guid", true);
ixDbEz.createIndex("feedItem", "ixFeedId", "feedId");
ixDbEz.createIndex("feedItem", "ixQueued", "queued");
ixDbEz.createObjStore("setting", "id", true);
ixDbEz.createIndex("setting", "ixName", "name", true);
});
//Create or Open the local IndexedDB database via ixDbEz
ixDbEz.startDB("podcastDb", 8, dbConfig, undefined, undefined, false);
})
.value('db', ixDbEz)
.service('dbNew', ['$q', '$rootScope', 'db', function($q, $rootScope, _db) {
var _getOne = function(store, identifier) {
var deferred = $q.defer();
_db.getCursor(store, function(ixDbCursorReq)
{
if(typeof ixDbCursorReq !== "undefined") {
ixDbCursorReq.onsuccess = function (e) {
var cursor = ixDbCursorReq.result || e.result;
if (cursor) {
deferred.resolve(cursor.value);
cursor.continue();
} else {
//TODO: not sure if this'll work, since it may both resolve and reject.
// May need to check if any were resolved or not first
// deferred.reject();
}
}
} else {
deferred.reject();
}
});
return deferred.promise;
};
return {
getOne: _getOne
};
}]);
angular.module('podcasts.updater', ['podcasts.settings'])
.run(['update', function(update) {
update.checkFeeds();
}])
.service('update', ['$timeout', '$log', 'settings', 'downloader', function($timeout, $log, settings, downloader) {
var checkFeeds = function() {
$log.info('Running Feed Check');
settings.get('refreshInterval', function(value) {
if (value.value > 0) {
var refreshInterval = value.value;
update();
if (refreshInterval > 0) {
$timeout(checkFeeds, refreshInterval);
}
}
});
};
function update() {
downloader.downloadAll(true);
}
return {
checkFeeds: checkFeeds,
update: update
};
}])
;
angular.module('podcasts.settings', ['podcasts.database'])
.run(['settings', function(settings) {
settings.init();
}])
.service('settings', ['db', function(db) {
var settings = {},
initialized = false,
waiting = [];
function _init() {
console.log('init settings');
db.getCursor("setting", function(ixDbCursorReq)
{
if(typeof ixDbCursorReq !== "undefined") {
ixDbCursorReq.onsuccess = function (e) {
var cursor = ixDbCursorReq.result || e.result;
if (cursor) {
settings[cursor.value.name] = cursor.value;
cursor.continue();
} else {
initialized = true;
for (var i = 0, l = waiting.length; i < l; i++) {
waiting[i]();
}
}
}
}
});
}
function _set(name, value, key) {
var setting;
if (key) {
setting = {'id': key, 'name': name, 'value': value};
if (settings[name]['id'] === key) {
settings[name] = setting;
} else {
//TODO: name changed, go through all settings and find the setting by id, and adjust it
}
} else {
setting = {'name': name, 'value': value};
settings[name] = setting;
//TODO: get id after inserting into DB
}
db.put("setting", setting);
}
function _get(name, onSuccess, onFailure) {
if (!initialized) {
waiting.push(function() {
_get(name, onSuccess, onFailure);
});
return;
}
if (!angular.isUndefined(settings[name])) {
onSuccess(settings[name]);
} else {
db.getCursor("setting", function(ixDbCursorReq)
{
if(typeof ixDbCursorReq !== "undefined") {
ixDbCursorReq.onsuccess = function (e) {
var cursor = ixDbCursorReq.result || e.result;
if (cursor) {
onSuccess(cursor.value);
} else {
if (typeof onFailure === 'function') {
onFailure();
}
}
};
ixDbCursorReq.onerror = function (e) {
console.log('didnt get setting');
onFailure();
};
}
}, function() { onFailure(); }, IDBKeyRange.only(name), undefined, 'ixName');
}
}
function _setAllValuesInScope(scope) {
if (angular.isObject(scope.settings.refreshInterval)) { // Took random setting here
return;
}
if (!initialized) {
waiting.push(function() {
_setAllValuesInScope(scope);
});
return;
}
angular.forEach(settings, function(setting, index) {
scope.settings[setting.name] = setting;
});
scope.$apply(); //TODO: this conflicts with digest when getting to the settings page a second time
}
return {
init: _init,
set: _set,
get: _get,
setAllValuesInScope: _setAllValuesInScope
};
}])
;
angular.module('podcasts.importer', [])
.service('google', ['$q', '$http', 'feeds', function($q, $http, feeds) {
return {
import: function(email, password) {
var google = this;
this.auth(email,
password)
.then(function(authId) {
return google.fetchSubscriptions(authId)
}, function() {
//TODO: display error
})
.then(function(subscriptions) {
google.addFeedsFromJsonResponse(subscriptions);
})
;
},
auth: function(email, password) {
var escapedEmail = encodeURIComponent(email),
escapedPassword = encodeURIComponent(password),
deferred = $q.defer();
$http.post(
'https://www.google.com/accounts/ClientLogin',
'Email=' + escapedEmail + '&Passwd=' + escapedPassword + '&service=reader',
{'headers': {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'}}
).success(function(response) {
response.split(/\r\n|\r|\n/).forEach(function(value, index) {
var valueSplit = value.split('=');
if ('Auth' === valueSplit[0]) {
deferred.resolve(valueSplit[1]);
}
});
}).error(function(data, status, headers, config) {
console.log(data, status, headers);
deferred.reject();
});
return deferred.promise;
},
fetchSubscriptions: function(authId) {
var deferred = $q.defer();
$http
.get(
'http://www.google.com/reader/api/0/subscription/list?output=json',
{'headers': {'Authorization': 'GoogleLogin auth=' + authId}}
)
.success(function(json) {
deferred.resolve(json);
})
.error(function(data, status, headers, config) {
deferred.reject();
})
;
return deferred.promise;
},
addFeedsFromJsonResponse: function(json) {
json.subscriptions.forEach(function(subscription, subscriptionIndex) {
if (subscription.categories.length <= 0) {
return false;
}
subscription.categories.forEach(function(category, categoryIndes) {
if ("Listen Subscriptions" === category.label) {
var feedUrl = subscription.id;
if ("feed/" === feedUrl.substring(0, 5)) {
feedUrl = feedUrl.substring(5);
}
feeds.add(feedUrl);
}
});
});
}
};
}])
;