You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

559 lines
15 KiB

10 years ago
10 years ago
10 years ago
9 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
9 years ago
10 years ago
10 years ago
10 years ago
  1. #!/usr/bin/env python2
  2. # -*- coding: utf-8 -*-
  3. """Youtubedlg module that contains util functions.
  4. Attributes:
  5. _RANDOM_OBJECT (object): Object that it's used as a default parameter.
  6. YOUTUBEDL_BIN (string): Youtube-dl binary filename.
  7. """
  8. from __future__ import unicode_literals
  9. import os
  10. import sys
  11. import json
  12. import locale
  13. import subprocess
  14. from .info import __appname__
  15. from .version import __version__
  16. _RANDOM_OBJECT = object()
  17. YOUTUBEDL_BIN = 'youtube-dl'
  18. if os.name == 'nt':
  19. YOUTUBEDL_BIN += '.exe'
  20. def get_encoding():
  21. """Return system encoding. """
  22. try:
  23. encoding = locale.getpreferredencoding()
  24. 'TEST'.encode(encoding)
  25. except:
  26. encoding = 'UTF-8'
  27. return encoding
  28. def convert_on_bounds(func):
  29. """Decorator to convert string inputs & outputs.
  30. Covert string inputs & outputs between 'str' and 'unicode' at the
  31. application bounds using the preferred system encoding. It will convert
  32. all the string params (args, kwargs) to 'str' type and all the
  33. returned strings values back to 'unicode'.
  34. """
  35. def convert_item(item, to_unicode=False):
  36. """The actual function which handles the conversion.
  37. Args:
  38. item (-): Can be any python item.
  39. to_unicode (boolean): When True it will convert all the 'str' types
  40. to 'unicode'. When False it will convert all the 'unicode'
  41. types back to 'str'.
  42. """
  43. if to_unicode and isinstance(item, str):
  44. # Convert str to unicode
  45. return item.decode(get_encoding(), 'ignore')
  46. if not to_unicode and isinstance(item, unicode):
  47. # Convert unicode to str
  48. return item.encode(get_encoding(), 'ignore')
  49. if hasattr(item, '__iter__'):
  50. # Handle iterables
  51. temp_list = []
  52. for sub_item in item:
  53. if isinstance(item, dict):
  54. temp_list.append((sub_item, covert_item(item[sub_item])))
  55. else:
  56. temp_list.append(convert_item(sub_item))
  57. return type(item)(temp_list)
  58. return item
  59. def wrapper(*args, **kwargs):
  60. returned_value = func(*convert_item(args), **convert_item(kwargs))
  61. return convert_item(returned_value, True)
  62. return wrapper
  63. # See: https://github.com/MrS0m30n3/youtube-dl-gui/issues/57
  64. # Patch os functions to convert between 'str' and 'unicode' on app bounds
  65. os_getenv = convert_on_bounds(os.getenv)
  66. os_makedirs = convert_on_bounds(os.makedirs)
  67. os_path_isdir = convert_on_bounds(os.path.isdir)
  68. os_path_exists = convert_on_bounds(os.path.exists)
  69. os_path_dirname = convert_on_bounds(os.path.dirname)
  70. os_path_abspath = convert_on_bounds(os.path.abspath)
  71. os_path_realpath = convert_on_bounds(os.path.realpath)
  72. os_path_expanduser = convert_on_bounds(os.path.expanduser)
  73. # Patch Windows specific functions
  74. if os.name == 'nt':
  75. os_startfile = convert_on_bounds(os.startfile)
  76. def remove_file(filename):
  77. if os_path_exists(filename):
  78. os.remove(filename)
  79. return True
  80. return False
  81. def remove_shortcuts(path):
  82. """Return given path after removing the shortcuts. """
  83. return path.replace('~', os_path_expanduser('~'))
  84. def absolute_path(filename):
  85. """Return absolute path to the given file. """
  86. return os_path_dirname(os_path_realpath(os_path_abspath(filename)))
  87. def open_file(file_path):
  88. """Open file in file_path using the default OS application.
  89. Returns:
  90. True on success else False.
  91. """
  92. file_path = remove_shortcuts(file_path)
  93. if not os_path_exists(file_path):
  94. return False
  95. if os.name == "nt":
  96. os_startfile(file_path)
  97. else:
  98. subprocess.call(("xdg-open", file_path))
  99. return True
  100. def encode_tuple(tuple_to_encode):
  101. """Turn size tuple into string. """
  102. return '%s/%s' % (tuple_to_encode[0], tuple_to_encode[1])
  103. def decode_tuple(encoded_tuple):
  104. """Turn tuple string back to tuple. """
  105. s = encoded_tuple.split('/')
  106. return int(s[0]), int(s[1])
  107. def check_path(path):
  108. """Create path if not exist. """
  109. if not os_path_exists(path):
  110. os_makedirs(path)
  111. def get_config_path():
  112. """Return user config path.
  113. Note:
  114. Windows = %AppData% + app_name
  115. Linux = ~/.config + app_name
  116. """
  117. if os.name == 'nt':
  118. path = os_getenv('APPDATA')
  119. else:
  120. path = os.path.join(os_path_expanduser('~'), '.config')
  121. return os.path.join(path, __appname__.lower())
  122. def shutdown_sys(password=None):
  123. """Shuts down the system.
  124. Returns True if no errors occur else False.
  125. Args:
  126. password (string): SUDO password for linux.
  127. Note:
  128. On Linux you need to provide sudo password if you don't
  129. have elevated privileges.
  130. """
  131. _stderr = subprocess.PIPE
  132. _stdin = None
  133. info = None
  134. encoding = get_encoding()
  135. if os.name == 'nt':
  136. cmd = ['shutdown', '/s', '/t', '1']
  137. # Hide subprocess window
  138. info = subprocess.STARTUPINFO()
  139. info.dwFlags |= subprocess.STARTF_USESHOWWINDOW
  140. else:
  141. if password:
  142. _stdin = subprocess.PIPE
  143. password = ('%s\n' % password).encode(encoding)
  144. cmd = ['sudo', '-S', '/sbin/shutdown', '-h', 'now']
  145. else:
  146. cmd = ['/sbin/shutdown', '-h', 'now']
  147. cmd = [item.encode(encoding, 'ignore') for item in cmd]
  148. shutdown_proc = subprocess.Popen(cmd,
  149. stderr=_stderr,
  150. stdin=_stdin,
  151. startupinfo=info)
  152. output = shutdown_proc.communicate(password)[1]
  153. return not output or output == "Password:"
  154. def to_string(data):
  155. """Convert data to string.
  156. Works for both Python2 & Python3. """
  157. return '%s' % data
  158. def get_time(seconds):
  159. """Convert given seconds to days, hours, minutes and seconds.
  160. Args:
  161. seconds (float): Time in seconds.
  162. Returns:
  163. Dictionary that contains the corresponding days, hours, minutes
  164. and seconds of the given seconds.
  165. """
  166. dtime = dict(seconds=0, minutes=0, hours=0, days=0)
  167. dtime['days'] = int(seconds / 86400)
  168. dtime['hours'] = int(seconds % 86400 / 3600)
  169. dtime['minutes'] = int(seconds % 86400 % 3600 / 60)
  170. dtime['seconds'] = int(seconds % 86400 % 3600 % 60)
  171. return dtime
  172. def get_locale_file():
  173. """Search for youtube-dlg locale file.
  174. Returns:
  175. The path to youtube-dlg locale file if exists else None.
  176. Note:
  177. Paths that get_locale_file() func searches.
  178. __main__ dir, library dir, /usr/share/youtube-dlg/locale
  179. """
  180. DIR_NAME = 'locale'
  181. SEARCH_DIRS = [
  182. os.path.join(absolute_path(sys.argv[0]), DIR_NAME),
  183. os.path.join(os_path_dirname(__file__), DIR_NAME),
  184. os.path.join('/usr', 'share', __appname__.lower(), DIR_NAME)
  185. ]
  186. if sys.platform == 'darwin':
  187. SEARCH_DIRS.append('/usr/local/Cellar/youtube-dl-gui/{version}/share/locale'.format(version=__version__))
  188. for directory in SEARCH_DIRS:
  189. if os_path_isdir(directory):
  190. return directory
  191. return None
  192. def get_icon_file():
  193. """Search for youtube-dlg app icon.
  194. Returns:
  195. The path to youtube-dlg icon file if exists, else returns None.
  196. Note:
  197. Paths that get_icon_file() function searches.
  198. __main__ dir, library dir, $XDG_DATA_DIRS
  199. """
  200. ICON_NAME = "youtube-dl-gui.png"
  201. SIZES = ("256", "128", "64", "48", "32", "16")
  202. DIR_TEMPLATE = os.path.join("icons", "hicolor", "{size}x{size}", "apps", ICON_NAME)
  203. ICONS = [DIR_TEMPLATE.format(size=size) for size in SIZES]
  204. search_dirs = [
  205. os.path.join(absolute_path(sys.argv[0]), "data"),
  206. os.path.join(os_path_dirname(__file__), "data"),
  207. ]
  208. # Append $XDG_DATA_DIRS on search_dirs
  209. path = os_getenv("XDG_DATA_DIRS")
  210. if path is not None:
  211. for xdg_path in path.split(':'):
  212. search_dirs.append(xdg_path)
  213. # Also append /usr/share/pixmaps on search_dirs
  214. #search_dirs.append("/usr/share/pixmaps")
  215. for directory in search_dirs:
  216. for icon in ICONS:
  217. icon_file = os.path.join(directory, icon)
  218. if os_path_exists(icon_file):
  219. return icon_file
  220. return None
  221. def get_pixmaps_dir():
  222. """Return absolute path to the pixmaps icons folder."""
  223. # TODO probably will need support for other directories py2exe etc
  224. return os.path.join(absolute_path(__file__), "data", "pixmaps")
  225. def json_load(filename):
  226. if os_path_exists(filename):
  227. with open(filename) as input_json_file:
  228. return json.load(input_json_file)
  229. return []
  230. def json_store(filename, item):
  231. with open(filename, 'w') as output_json_file:
  232. json.dump(item, output_json_file)
  233. def read_formats():
  234. """Returns a twodict containing all the formats from 'data/formats'."""
  235. # TODO Support for other directories? Test with py2exe
  236. formats_file = os.path.join(absolute_path(__file__), "data", "formats")
  237. if os_path_exists(formats_file):
  238. formats_dict = TwoWayOrderedDict()
  239. with open(formats_file) as input_file:
  240. for line in input_file:
  241. format_id, format_label = line.split('-')
  242. formats_dict[format_id.strip()] = format_label.strip()
  243. return formats_dict
  244. return None
  245. class TwoWayOrderedDict(dict):
  246. """Custom data structure which implements a two way ordrered dictionary.
  247. TwoWayOrderedDict it's a custom dictionary in which you can get the
  248. key:value relationship but you can also get the value:key relationship.
  249. It also remembers the order in which the items were inserted and supports
  250. almost all the features of the build-in dict.
  251. Note:
  252. Ways to create a new dictionary.
  253. *) d = TwoWayOrderedDict(a=1, b=2) (Unordered)
  254. *) d = TwoWayOrderedDict({'a': 1, 'b': 2}) (Unordered)
  255. *) d = TwoWayOrderedDict([('a', 1), ('b', 2)]) (Ordered)
  256. *) d = TwoWayOrderedDict(zip(['a', 'b', 'c'], [1, 2, 3])) (Ordered)
  257. Examples:
  258. >>> d = TwoWayOrderedDict(a=1, b=2)
  259. >>> d['a']
  260. 1
  261. >>> d[1]
  262. 'a'
  263. >>> print d
  264. TwoWayOrderedDict([('a', 1), ('b', 2)])
  265. """
  266. _PREV = 0
  267. _KEY = 1
  268. _NEXT = 2
  269. def __init__(self, *args, **kwargs):
  270. self._items = item = []
  271. self._items += [item, None, item] # Double linked list [prev, key, next]
  272. self._items_map = {} # Map link list items into keys to speed up lookup
  273. self._load(args, kwargs)
  274. def __setitem__(self, key, value):
  275. if key in self:
  276. # If self[key] == key for example {'b': 'b'} and we
  277. # do d['b'] = 2 then we dont want to remove the 'b'
  278. # from our linked list because we will lose the order
  279. if self[key] in self._items_map and key != self[key]:
  280. self._remove_mapped_key(self[key])
  281. dict.__delitem__(self, self[key])
  282. if value in self:
  283. # If value == key we dont have to remove the
  284. # value from the items_map because the value is
  285. # the key and we want to keep the key in our
  286. # linked list in order to keep the order.
  287. if value in self._items_map and key != value:
  288. self._remove_mapped_key(value)
  289. if self[value] in self._items_map:
  290. self._remove_mapped_key(self[value])
  291. # Check if self[value] is in the dict
  292. # for cases like {'a': 'a'} where we
  293. # have only one copy instead of {'a': 1, 1: 'a'}
  294. if self[value] in self:
  295. dict.__delitem__(self, self[value])
  296. if key not in self._items_map:
  297. last = self._items[self._PREV] # self._items prev always points to the last item
  298. last[self._NEXT] = self._items[self._PREV] = self._items_map[key] = [last, key, self._items]
  299. dict.__setitem__(self, key, value)
  300. dict.__setitem__(self, value, key)
  301. def __delitem__(self, key):
  302. if self[key] in self._items_map:
  303. self._remove_mapped_key(self[key])
  304. if key in self._items_map:
  305. self._remove_mapped_key(key)
  306. dict.__delitem__(self, self[key])
  307. # Check if key is in the dict
  308. # for cases like {'a': 'a'} where we
  309. # have only one copy instead of {'a': 1, 1: 'a'}
  310. if key in self:
  311. dict.__delitem__(self, key)
  312. def __len__(self):
  313. return len(self._items_map)
  314. def __iter__(self):
  315. curr = self._items[self._NEXT]
  316. while curr is not self._items:
  317. yield curr[self._KEY]
  318. curr = curr[self._NEXT]
  319. def __reversed__(self):
  320. curr = self._items[self._PREV]
  321. while curr is not self._items:
  322. yield curr[self._KEY]
  323. curr = curr[self._PREV]
  324. def __repr__(self):
  325. return '%s(%r)' % (self.__class__.__name__, self.items())
  326. def __eq__(self, other):
  327. if isinstance(other, self.__class__):
  328. return self.items() == other.items()
  329. return False
  330. def __ne__(self, other):
  331. return not self == other
  332. def _remove_mapped_key(self, key):
  333. """Remove the given key both from the linked list
  334. and the map dictionary. """
  335. prev, __, next = self._items_map.pop(key)
  336. prev[self._NEXT] = next
  337. next[self._PREV] = prev
  338. def _load(self, args, kwargs):
  339. """Load items into our dictionary. """
  340. for item in args:
  341. if type(item) == dict:
  342. item = item.iteritems()
  343. for key, value in item:
  344. self[key] = value
  345. for key, value in kwargs.items():
  346. self[key] = value
  347. def items(self):
  348. return [(key, self[key]) for key in self]
  349. def values(self):
  350. return [self[key] for key in self]
  351. def keys(self):
  352. return list(self)
  353. def pop(self, key, default=_RANDOM_OBJECT):
  354. try:
  355. value = self[key]
  356. del self[key]
  357. except KeyError as error:
  358. if default == _RANDOM_OBJECT:
  359. raise error
  360. value = default
  361. return value
  362. def popitem(self, last=True):
  363. """Remove and return a (key, value) pair from the dictionary.
  364. If the dictionary is empty calling popitem() raises a KeyError.
  365. Args:
  366. last (bool): When False popitem() will remove the first item
  367. from the list.
  368. Note:
  369. popitem() is useful to destructively iterate over a dictionary.
  370. Raises:
  371. KeyError
  372. """
  373. if not self:
  374. raise KeyError('popitem(): dictionary is empty')
  375. if last:
  376. __, key, __ = self._items[self._PREV]
  377. else:
  378. __, key, __ = self._items[self._NEXT]
  379. value = self.pop(key)
  380. return key, value
  381. def update(self, *args, **kwargs):
  382. self._load(args, kwargs)
  383. def setdefault(self, key, default=None):
  384. try:
  385. return self[key]
  386. except KeyError:
  387. self[key] = default
  388. return default
  389. def copy(self):
  390. return self.__class__(self.items())
  391. def clear(self):
  392. self._items = item = []
  393. self._items += [item, None, item]
  394. self._items_map = {}
  395. dict.clear(self)