├── tests ├── __init__.py ├── runtests.py ├── test_util.py ├── test_common.py └── util.py ├── lulu ├── util │ ├── __init__.py │ ├── parser.py │ ├── strings.py │ ├── term.py │ ├── git.py │ ├── fs.py │ └── log.py ├── cli_wrapper │ ├── __init__.py │ ├── player │ │ ├── wmp.py │ │ ├── mplayer.py │ │ ├── dragonplayer.py │ │ ├── gnome_mplayer.py │ │ ├── vlc.py │ │ └── __init__.py │ └── transcoder │ │ ├── ffmpeg.py │ │ ├── libav.py │ │ ├── __init__.py │ │ └── mencoder.py ├── version.py ├── processor │ ├── __init__.py │ ├── join_ts.py │ └── rtmpdump.py ├── __init__.py ├── extractors │ ├── khan.py │ ├── joy.py │ ├── dilidili.py │ ├── tudou.py │ ├── cbs.py │ ├── alive.py │ ├── archive.py │ ├── ku6.py │ ├── giphy.py │ ├── freesound.py │ ├── magisto.py │ ├── ehow.py │ ├── huomaotv.py │ ├── quanmin.py │ ├── douyin.py │ ├── bandcamp.py │ ├── iqilu.py │ ├── metacafe.py │ ├── ted.py │ ├── bcy.py │ ├── heavymusic.py │ ├── pixivision.py │ ├── kuaishou.py │ ├── musicplayon.py │ ├── facebook.py │ ├── icourses.py │ ├── theplatform.py │ ├── iwara.py │ ├── naver.py │ ├── veoh.py │ ├── vidto.py │ ├── kuwo.py │ ├── vine.py │ ├── dailymotion.py │ ├── w56.py │ ├── douban.py │ ├── suntv.py │ ├── baomihua.py │ ├── yizhibo.py │ ├── qq_egame.py │ ├── qingting.py │ ├── soundcloud.py │ ├── panda.py │ ├── mtv81.py │ ├── videomega.py │ ├── kugou.py │ ├── ifeng.py │ ├── fantasy.py │ ├── vk.py │ ├── nicovideo.py │ ├── nanagogo.py │ ├── pinterest.py │ ├── infoq.py │ ├── __init__.py │ ├── yinyuetai.py │ ├── pixnet.py │ ├── yixia.py │ ├── fc2video.py │ ├── huaban.py │ ├── lizhi.py │ ├── toutiao.py │ ├── showroom.py │ ├── instagram.py │ ├── zhanqi.py │ ├── bigthink.py │ ├── tucao.py │ ├── weibo.py │ ├── qie_video.py │ ├── cntv.py │ ├── douyutv.py │ ├── sohu.py │ ├── imgur.py │ ├── ckplayer.py │ ├── ixigua.py │ ├── longzhu.py │ ├── bokecc.py │ ├── qie.py │ ├── wanmen.py │ ├── twitter.py │ ├── ximalaya.py │ ├── coub.py │ └── google.py ├── json_output.py └── __main__.py ├── MANIFEST.in ├── lulu.plugin.zsh ├── Pipfile ├── codecov.yml ├── bin └── lulu ├── fabfile.py ├── .travis.yml ├── appveyor.yml ├── .coveragerc ├── contrib └── completion │ ├── lulu-completion.bash │ ├── lulu.fish │ └── _lulu ├── LICENSE ├── README.rst ├── .gitignore └── setup.py /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lulu/util/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lulu/cli_wrapper/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lulu/cli_wrapper/player/wmp.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lulu/cli_wrapper/player/mplayer.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lulu/cli_wrapper/transcoder/ffmpeg.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lulu/cli_wrapper/transcoder/libav.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE 2 | -------------------------------------------------------------------------------- /lulu/cli_wrapper/player/dragonplayer.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lulu/cli_wrapper/player/gnome_mplayer.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lulu/cli_wrapper/transcoder/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lulu/cli_wrapper/transcoder/mencoder.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lulu/cli_wrapper/player/vlc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | -------------------------------------------------------------------------------- /lulu/cli_wrapper/player/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from .mplayer import * 4 | -------------------------------------------------------------------------------- /lulu/version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | __version__ = '0.5.3' 4 | script_name = 'lulu' 5 | -------------------------------------------------------------------------------- /lulu.plugin.zsh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | alias lulu="noglob python3 $(dirname $0)/bin/lulu" 3 | alias lulu-vlc="noglob python3 $(dirname $0)/bin/lulu --player vlc" 4 | -------------------------------------------------------------------------------- /lulu/processor/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from .join_flv import concat_flv 4 | from .join_mp4 import concat_mp4 5 | from .ffmpeg import * 6 | from .rtmpdump import * 7 | -------------------------------------------------------------------------------- /lulu/util/parser.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | from bs4 import BeautifulSoup 4 | 5 | 6 | def get_parser(page): 7 | parser = BeautifulSoup(page, 'html.parser') 8 | return parser 9 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | 3 | url = "https://pypi.python.org/simple" 4 | verify_ssl = true 5 | 6 | 7 | [dev-packages] 8 | 9 | twine = "*" 10 | ipdb = "*" 11 | 12 | 13 | [packages] 14 | 15 | coverage = "==4.4.2" 16 | cryptography = "==2.1.4" 17 | beautifulsoup4 = "==4.6.0" 18 | requests = "==2.18.4" 19 | -------------------------------------------------------------------------------- /lulu/util/strings.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | from html import unescape 4 | 5 | from lulu.util.fs import legitimize 6 | 7 | 8 | def get_filename(htmlstring): 9 | return legitimize(unescape(htmlstring)) 10 | 11 | 12 | def parameterize(string): 13 | return "'{}'".format(string.replace("'", r"'\''")) 14 | -------------------------------------------------------------------------------- /tests/runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf-8 3 | 4 | import unittest 5 | 6 | 7 | TEST_MODULES = [ 8 | 'tests.test_util', 9 | 'tests.test_common', 10 | 'tests.test_extractors', 11 | ] 12 | 13 | 14 | def all(): 15 | return unittest.defaultTestLoader.loadTestsFromNames(TEST_MODULES) 16 | 17 | 18 | if __name__ == '__main__': 19 | unittest.main(defaultTest='all') 20 | -------------------------------------------------------------------------------- /lulu/util/term.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | def get_terminal_size(): 5 | """Get (width, height) of the current terminal.""" 6 | try: 7 | # fcntl module only available on Unix 8 | import fcntl 9 | import termios 10 | import struct 11 | return struct.unpack('hh', fcntl.ioctl(1, termios.TIOCGWINSZ, '1234')) 12 | except Exception: 13 | return (40, 80) 14 | -------------------------------------------------------------------------------- /lulu/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # This file is Python 2 compliant. 3 | 4 | import sys 5 | 6 | if sys.version_info[0] == 3: 7 | #from .extractor import Extractor, VideoExtractor 8 | #from .util import log 9 | 10 | from .__main__ import * 11 | 12 | #from .common import * 13 | #from .version import * 14 | #from .cli_wrapper import * 15 | #from .extractor import * 16 | else: 17 | # Don't import anything. 18 | pass 19 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | token: c430fe31-42ab-4fa4-81c3-f4109f1912a1 3 | 4 | coverage: 5 | precision: 2 6 | round: down 7 | range: "70...100" 8 | 9 | status: 10 | project: 11 | default: on 12 | patch: 13 | default: on 14 | changes: 15 | default: off 16 | 17 | comment: 18 | layout: "header, reach, diff, flags, files, footer" 19 | behavior: default 20 | require_changes: no 21 | require_base: no 22 | require_head: yes 23 | -------------------------------------------------------------------------------- /bin/lulu: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | 6 | _srcdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 7 | _filepath = os.path.dirname(sys.argv[0]) 8 | sys.path.insert(1, os.path.join(_filepath, _srcdir)) 9 | 10 | 11 | if sys.version_info[0] == 3: 12 | import lulu 13 | if __name__ == '__main__': 14 | lulu.main() 15 | else: # Python 2 16 | from lulu.util import log 17 | log.e('[fatal] Python 3 is required!') 18 | -------------------------------------------------------------------------------- /lulu/util/git.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | 5 | 6 | def get_head(repo_path): 7 | """Get (branch, commit) from HEAD of a git repo.""" 8 | try: 9 | ref = open( 10 | os.path.join(repo_path, '.git', 'HEAD'), 'r' 11 | ).read().strip()[5:].split('/') 12 | branch = ref[-1] 13 | commit = open( 14 | os.path.join(repo_path, '.git', *ref), 'r' 15 | ).read().strip()[:7] 16 | return branch, commit 17 | except Exception: 18 | return None 19 | -------------------------------------------------------------------------------- /fabfile.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | from fabric.api import ( 4 | local, 5 | ) 6 | 7 | 8 | def test(proxy=False): 9 | cmd = 'PYTHONPATH=./ {} coverage run tests/runtests.py'.format( 10 | 'proxychains4 -q' if proxy else '' 11 | ) 12 | local(cmd) 13 | 14 | 15 | def test_download(func): 16 | ''' 17 | fab test_download:acfun 18 | ''' 19 | cmd = ( 20 | 'PYTHONPATH=./ python tests/test_extractors.py ' 21 | 'TestExtractors.test_{}'.format(func) 22 | ) 23 | local(cmd) 24 | 25 | 26 | def upload(): 27 | local( 28 | 'python3 setup.py upload' 29 | ) 30 | -------------------------------------------------------------------------------- /tests/test_util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import unittest 4 | 5 | from lulu.util import fs 6 | from lulu.util import parser 7 | 8 | 9 | class TestUtil(unittest.TestCase): 10 | def test_legitimize(self): 11 | self.assertEqual(fs.legitimize('1*2', os='Linux'), '1*2') 12 | self.assertEqual(fs.legitimize('1*2', os='Darwin'), '1*2') 13 | self.assertEqual(fs.legitimize('1*2', os='Windows'), '1-2') 14 | 15 | def test_parser(self): 16 | p = parser.get_parser('

hello

') 17 | self.assertEqual(p.h1.string.strip(), 'hello') 18 | 19 | 20 | if __name__ == '__main__': 21 | unittest.main() 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | 3 | matrix: 4 | fast_finish: true 5 | 6 | sudo: false 7 | 8 | language: python 9 | 10 | python: 11 | - 3.5 12 | - 3.6 13 | 14 | cache: 15 | pip: true 16 | 17 | # command to install dependencies 18 | install: 19 | - pip install codecov 20 | - pip install pipenv 21 | 22 | before_script: 23 | - export CI=True 24 | - export TRAVIS=True 25 | - export PYTHONPATH=$PYTHONPATH:$(pwd) 26 | - pipenv install 27 | 28 | # command to run tests 29 | script: coverage run tests/runtests.py 30 | 31 | after_success: 32 | - codecov 33 | 34 | branches: 35 | only: 36 | - master 37 | 38 | notifications: 39 | email: false 40 | -------------------------------------------------------------------------------- /tests/test_common.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import unittest 4 | 5 | from lulu.common import ( 6 | match1, 7 | ) 8 | 9 | 10 | class TestCommon(unittest.TestCase): 11 | 12 | def test_match1(self): 13 | self.assertEqual( 14 | match1('http://youtu.be/1234567890A', r'youtu.be/([^/]+)'), 15 | '1234567890A' 16 | ) 17 | self.assertEqual( 18 | match1( 19 | 'http://youtu.be/1234567890A', 20 | r'youtu.be/([^/]+)', r'youtu.(\w+)' 21 | ), 22 | ['1234567890A', 'be'] 23 | ) 24 | 25 | 26 | if __name__ == '__main__': 27 | unittest.main() 28 | -------------------------------------------------------------------------------- /lulu/extractors/khan.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | from lulu.common import ( 5 | get_content, 6 | playlist_not_supported, 7 | ) 8 | from lulu.util.parser import get_parser 9 | from lulu.extractors.youtube import YouTube 10 | 11 | 12 | __all__ = ['khan_download'] 13 | site_info = 'Khan Academy khanacademy.org' 14 | 15 | 16 | def khan_download(url, **kwargs): 17 | html = get_content(url) 18 | parser = get_parser(html) 19 | youtube_url = parser.find('meta', property='og:video')['content'] 20 | YouTube().download_by_url(youtube_url, **kwargs) 21 | 22 | 23 | download = khan_download 24 | download_playlist = playlist_not_supported(site_info) 25 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - PYTHON: C:\Python35 4 | - PYTHON: C:\Python36 5 | 6 | build: off 7 | 8 | init: 9 | - SET CI=True 10 | - SET APPVEYOR=True 11 | - SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH% 12 | - SET PYTHONIOENCODING=utf-8 13 | - SET PYTHONPATH=%APPVEYOR_BUILD_FOLDER% 14 | 15 | install: 16 | - pip install -U pip 17 | - pip install pipenv 18 | - pipenv install 19 | 20 | test_script: 21 | - pipenv run coverage run tests/runtests.py 22 | 23 | branches: 24 | only: 25 | - master 26 | 27 | notifications: 28 | - provider: Email 29 | on_build_success: false 30 | on_build_failure: false 31 | on_build_status_changed: false 32 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | # .coveragerc to control coverage.py 2 | [run] 3 | branch = True 4 | source = 5 | lulu 6 | tests 7 | 8 | [report] 9 | # Regexes for lines to exclude from consideration 10 | exclude_lines = 11 | # Have to re-enable the standard pragma 12 | pragma: no cover 13 | 14 | # Don't complain about missing debug-only code: 15 | def __repr__ 16 | if self\.debug 17 | 18 | # Don't complain if tests don't hit defensive assertion code: 19 | raise AssertionError 20 | raise NotImplementedError 21 | 22 | # Don't complain if non-runnable code isn't run: 23 | if 0: 24 | if __name__ == .__main__.: 25 | 26 | ignore_errors = True 27 | 28 | [html] 29 | directory = coverage_html_report 30 | -------------------------------------------------------------------------------- /lulu/extractors/joy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from lulu.common import ( 4 | url_info, 5 | print_info, 6 | get_content, 7 | download_urls, 8 | playlist_not_supported, 9 | ) 10 | from lulu.util.parser import get_parser 11 | 12 | 13 | __all__ = ['joy_download'] 14 | site_info = '激动网 joy.cn' 15 | 16 | 17 | def joy_download(url, info_only=False, **kwargs): 18 | page = get_content(url) 19 | parser = get_parser(page) 20 | url = parser.source['src'] 21 | title = parser.h1.text.strip() 22 | _, ext, size = url_info(url) 23 | print_info(site_info, title, ext, size) 24 | if not info_only: 25 | download_urls([url], title, ext, size, **kwargs) 26 | 27 | 28 | download = joy_download 29 | download_playlist = playlist_not_supported(site_info) 30 | -------------------------------------------------------------------------------- /lulu/extractors/dilidili.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from lulu.common import ( 4 | match1, 5 | get_content, 6 | any_download, 7 | playlist_not_supported, 8 | ) 9 | 10 | 11 | __all__ = ['dilidili_download'] 12 | site_info = 'dilidili.com' 13 | 14 | 15 | def dilidili_download(url, **kwargs): 16 | html = get_content(url) 17 | # player loaded via internal iframe 18 | # http://www.maoyun.tv/mdparse/index.php?id=http://v.youku.com/v_show/id_XMTYxNzk0NjUzMg==.html # noqa 19 | iframe_url = match1(html, r'