Commit 15327345 authored by Lysander Trischler's avatar Lysander Trischler

Initial commit

parents
# Temporary editor files
*.swn
*.swo
*.swp
*.backup
*~
# Python
*.pyc
*.pyo
#!/usr/bin/python
# encoding: utf-8
#
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
# Version 2, December 2004
#
# Copyright (C) 2011 Lysander Trischler <software@lyse.isobeef.org>
#
# Everyone is permitted to copy and distribute verbatim or modified
# copies of this license document, and changing it is allowed as long
# as the name is changed.
#
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
#
# 0. You just DO WHAT THE FUCK YOU WANT TO.
#
"""
yfav – publish your favorite YouTube videos via an Atom feed
Usage: yfav [ VIDEO_ID ... ]
"""
__author = "Lysander Trischler"
__copyright__ = "Copyright 2012, Lysander Trischler"
__license__ = "WTFPL"
__maintainer__ = "Lysander Trischler"
__email__ = "software@lyse.isobeef.org"
__version__ = "0.1"
__version_info__ = (0, 1)
import os.path
import os
import time
import ConfigParser
import urlparse
import urllib2
class Config(ConfigParser.ConfigParser):
def __init__(self, fp):
ConfigParser.ConfigParser.__init__(self)
self.read([fp])
def get(self, section, field, default):
try:
return ConfigParser.ConfigParser.get(self, section, field)
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
return default
def path(*path):
return os.path.join(*[os.path.expanduser(segment.replace("$HOME", "~")) for segment in path])
DATA_DIR = path(os.getenv("XDG_DATA_HOME", "~/.local/share"), "yfav")
CONFIG_FILE = path(os.getenv("XDG_CONFIG_HOME", "~/.config"), "yfav") # never used
INDEX_FILE = path(DATA_DIR, "index")
ATOM_FILE = path(DATA_DIR, "atom")
VIDEO_ID_LEN = 11
VIDEO_ID_URL = "http://youtube.com/watch?v="
CONFIG = Config(CONFIG_FILE)
def now():
return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
import re
# stolen from tornado.escape
_XHTML_ESCAPE_RE = re.compile('[&<>"]')
_XHTML_ESCAPE_DICT = {'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;'}
def esc(value):
"""Escapes a string so it is valid within XML or XHTML."""
if value is None: return u''
return _XHTML_ESCAPE_RE.sub(lambda match: _XHTML_ESCAPE_DICT[match.group(0)], value)
class Video(object):
def __init__(self, line=None, id=None, title=None, updated=None, author=None):
if line is not None:
self.id, self.title, self.updated, self.author = line.rstrip().split("\t", 3)
else:
self.id = Video.extract_video_id(id)
self.title = title
self.updated = updated
self.author = author
if not self.id:
raise ValueError("No video ID given!")
if not self.updated:
self.updated = now()
if not self.title or not self.author:
info = Video.get_info(self.id)
if not self.title:
self.title = info.get('title', ['<Unknown title>'])[0]
if not self.author:
self.author = info.get('author', ['<Unknown author>'])[0]
@classmethod
def get_info(cls, video_id):
info = urllib2.urlopen('http://youtube.com/get_video_info?video_id=%s' % video_id)
data = urlparse.parse_qs(info.read())
info.close()
return data
@classmethod
def extract_video_id(cls, video_id):
if len(video_id) == VIDEO_ID_LEN:
return video_id
if video_id.startswith(VIDEO_ID_URL) and len(video_id) == len(VIDEO_ID_URL) + VIDEO_ID_LEN:
return video_id[len(VIDEO_ID_URL):]
raise ValueError("%r is a potential illegal YouTube video ID!" % video_id)
@property
def url(self):
return "http://youtube.com/watch?v=%s" % self.id
@property
def line(self):
return '%s\t%s\t%s\t%s\n' % (self.id, self.title, self.updated, self.author)
def add_fav(video_id):
video_id = Video.extract_video_id(video_id)
if not os.path.exists(INDEX_FILE):
index_dirname = os.path.dirname(INDEX_FILE)
if not os.path.exists(index_dirname):
os.makedirs(index_dirname)
else:
# check duplicates
with open(INDEX_FILE, "r") as index:
for line in index.readlines():
video = Video(line=line)
if video.id == video_id:
return
video = Video(id=video_id)
with open(INDEX_FILE, "a") as index:
index.write(video.line)
def gen_atom(max=None):
if not os.path.exists(INDEX_FILE):
return
atom_dirname = os.path.dirname(ATOM_FILE)
if not os.path.exists(atom_dirname):
os.makedirs(atom_dirname)
with open(INDEX_FILE, "r") as index, open(ATOM_FILE, "w") as atom:
atom.write('<?xml version="1.0" encoding="utf-8"?>'
'<feed xmlns="http://www.w3.org/2005/Atom">'
'<link rel="self" type="application/atom+xml" href="' + CONFIG.get("atom", "url", "https://lyse.isobeef.org/yfav.atom") + '" />'
'<id>' + CONFIG.get("atom", "id", "https://lyse.isobeef.org/yfav.atom") + '</id>'
'<title>' + CONFIG.get("atom", "title", "Lyse's favorite YouTube videos") + '</title>'
'<updated>' + now() + '</updated>'
'<generator>yfav/' + __version_info__ + '</generator>\n')
lines = reversed(index.readlines())
if max is not None:
lines = lines[:max]
for line in lines:
video = Video(line=line)
atom.write('<entry><id>' + esc(video.url) + '</id>'
'<title>' + esc(video.title) + '</title>'
'<updated>' + esc(video.updated) + '</updated>'
'<author><name>' + esc(video.author) + '</name></author>'
'<link rel="alternate" href="' + esc(video.url) + '" /></entry>\n')
atom.write('</feed>\n')
def upload():
os.system("scp -q " + ATOM_FILE
+ " " + CONFIG.get("scp", "user", "lyse")
+ "@" + CONFIG.get("scp", "host", "isobeef.org")
+ ":" + CONFIG.get("scp", "path", "srv/yfav.atom"))
if __name__ == "__main__":
import sys
if len(sys.argv) > 1:
for video_id in sys.argv[1:]:
add_fav(video_id)
else:
if sys.stdin.isatty():
while True:
try:
video_id = raw_input("YouTube Video ID > ")
if video_id:
add_fav(video_id)
except EOFError:
break
else:
for video_id in sys.stdin.readlines():
add_fav(video_id.strip())
gen_atom()
upload()
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment