コード例 #1
0
ファイル: player.py プロジェクト: beam/service.upnext
class Player(xbmc.Player):
    last_file = None
    track = False

    def __init__(self):
        self.api = Api()
        self.developer = Developer()
        xbmc.Player.__init__(self)

    def set_last_file(self, file):
        self.last_file = file

    def get_last_file(self):
        return self.last_file

    def is_tracking(self):
        return self.track

    def disable_tracking(self):
        self.track = False

    def onPlayBackStarted(self):
        # Will be called when kodi starts playing a file
        self.track = True
        if utils.settings("developerMode") == "true":
            self.developer.developer_play_back()

    def onPlayBackStopped(self):
        # Will be called when user stops playing a file.
        self.last_file = None
        self.disable_tracking()
        self.api.reset_addon_data()
        State() # reset state
コード例 #2
0
ファイル: monitor.py プロジェクト: beam/service.upnext
class Monitor(xbmc.Monitor):

    def __init__(self):
        self.player = Player()
        self.api = Api()
        self.playback_manager = PlaybackManager()
        xbmc.Monitor.__init__(self)

    def log(self, msg, lvl=1):
        class_name = self.__class__.__name__
        utils.log("%s %s" % (utils.addon_name(), class_name), str(msg), int(lvl))

    def run(self):

        while not self.abortRequested():
            # check every 1 sec
            if self.waitForAbort(1):
                # Abort was requested while waiting. We should exit
                break
            if self.player.is_tracking():
                try:
                    play_time = self.player.getTime()
                    total_time = self.player.getTotalTime()
                    last_file = self.player.get_last_file()
                    current_file = self.player.getPlayingFile()
                    notification_time = self.api.notification_time()
                    up_next_disabled = utils.settings("disableNextUp") == "true"
                    if utils.window("PseudoTVRunning") != "True" and not up_next_disabled and total_time > 300:
                        if (total_time - play_time <= int(notification_time) and (
                                last_file is None or last_file != current_file)) and total_time != 0:
                            self.player.set_last_file(current_file)
                            self.log("Calling autoplayback totaltime - playtime is %s" % (total_time - play_time), 2)
                            self.playback_manager.launch_up_next()
                            self.log("Up Next style autoplay succeeded.", 2)
                            self.player.disable_tracking()

                except Exception as e:
                    self.log("Exception in Playback Monitor Service: %s" % repr(e))

        self.log("======== STOP %s ========" % utils.addon_name(), 0)

    def onNotification(self, sender, method, data):

        if method.split('.')[1].lower() != 'upnext_data':  # method looks like Other.upnext_data
            return

        data = utils.decode_data(data)
        data['id'] = "%s_play_action" % str(sender.replace(".SIGNAL", ""))

        self.api.addon_data_received(data)
コード例 #3
0
ファイル: playItem.py プロジェクト: beam/service.upnext
class PlayItem:
    _shared_state = {}

    def __init__(self):
        self.__dict__ = self._shared_state
        self.api = Api()
        self.player = Player()
        self.state = State()

    def log(self, msg, lvl=2):
        class_name = self.__class__.__name__
        utils.log("%s %s" % (utils.addon_name(), class_name), msg, int(lvl))

    def get_episode(self):
        current_file = self.player.getPlayingFile()
        if not self.api.has_addon_data():
            # Get the active player
            result = self.api.get_now_playing()
            self.handle_now_playing_result(result)
            # get the next episode from kodi
            episode = (
                self.api.handle_kodi_lookup_of_episode(
                    self.state.tv_show_id, current_file, self.state.include_watched, self.state.current_episode_id))
        else:
            episode = self.api.handle_addon_lookup_of_next_episode()
            current_episode = self.api.handle_addon_lookup_of_current_episode()
            self.state.current_episode_id = current_episode["episodeid"]
            if self.state.current_tv_show_id != current_episode["tvshowid"]:
                self.state.current_tv_show_id = current_episode["tvshowid"]
                self.state.played_in_a_row = 1
        return episode

    def handle_now_playing_result(self, result):
        if 'result' in result:
            item_type = result["result"]["item"]["type"]
            current_episode_number = result["result"]["item"]["episode"]
            current_season_id = result["result"]["item"]["season"]
            current_show_title = result["result"]["item"]["showtitle"].encode('utf-8')
            current_show_title = utils.unicode_to_ascii(current_show_title)
            self.state.tv_show_id = result["result"]["item"]["tvshowid"]
            if item_type == "episode":
                if int(self.state.tv_show_id) == -1:
                    self.state.tv_show_id = self.api.showtitle_to_id(title=current_show_title)
                    self.log("Fetched missing tvshowid " + json.dumps(self.state.tv_show_id), 2)

                # Get current episodeid
                current_episode_id = self.api.get_episode_id(
                    showid=str(self.state.tv_show_id), show_season=current_season_id,
                    show_episode=current_episode_number)
                self.state.current_episode_id = current_episode_id
                if self.state.current_tv_show_id != self.state.tv_show_id:
                    self.state.current_tv_show_id = self.state.tv_show_id
                    self.state.played_in_a_row = 1
コード例 #4
0
 def __init__(self):
     self.player = Player()
     self.api = Api()
     self.playback_manager = PlaybackManager()
     xbmc.Monitor.__init__(self)
コード例 #5
0
ファイル: player.py プロジェクト: spcxsat/service.upnext
 def __init__(self):
     self.api = Api()
     self.state = State()
     self.developer = Developer()
     xbmc.Player.__init__(self)
コード例 #6
0
 def __init__(self):
     self.__dict__ = self._shared_state
     self.api = Api()
     self.play_item = PlayItem()
     self.state = State()
     self.player = Player()
コード例 #7
0
class PlaybackManager:
    _shared_state = {}

    def __init__(self):
        self.__dict__ = self._shared_state
        self.api = Api()
        self.play_item = PlayItem()
        self.state = State()
        self.player = Player()

    def log(self, msg, lvl=2):
        class_name = self.__class__.__name__
        utils.log("%s %s" % (utils.addon_name(), class_name), msg, int(lvl))

    def launch_up_next(self):
        episode = self.play_item.get_episode()
        if episode is None:
            # no episode get out of here
            self.log("Error: no episode could be found to play next...exiting",
                     1)
            return
        self.log("episode details %s" % json.dumps(episode), 2)
        self.launch_popup(episode)
        self.api.reset_addon_data()

    def launch_popup(self, episode):
        episode_id = episode["episodeid"]
        no_play_count = episode["playcount"] is None or episode[
            "playcount"] == 0
        include_play_count = True if self.state.include_watched else no_play_count
        if include_play_count and self.state.current_episode_id != episode_id:
            # we have a next up episode choose mode
            next_up_page, still_watching_page = pages.set_up_pages()
            showing_next_up_page, showing_still_watching_page, total_time = (
                self.show_popup_and_wait(episode, next_up_page,
                                         still_watching_page))
            should_play_default, should_play_non_default = (
                self.extract_play_info(next_up_page, showing_next_up_page,
                                       showing_still_watching_page,
                                       still_watching_page, total_time))

            play_item_option_1 = (should_play_default
                                  and self.state.playMode == "0")
            play_item_option_2 = (should_play_non_default
                                  and self.state.playMode == "1")
            if play_item_option_1 or play_item_option_2:
                self.log("playing media episode", 2)
                # Signal to trakt previous episode watched
                utils.event("NEXTUPWATCHEDSIGNAL",
                            {'episodeid': self.state.current_episode_id})
                # Play media
                if not self.api.has_addon_data():
                    self.api.play_kodi_item(episode)
                else:
                    self.api.play_addon_item()

    def show_popup_and_wait(self, episode, next_up_page, still_watching_page):
        play_time = self.player.getTime()
        total_time = self.player.getTotalTime()
        progress_step_size = utils.calculate_progress_steps(total_time -
                                                            play_time)
        next_up_page.setItem(episode)
        next_up_page.setProgressStepSize(progress_step_size)
        still_watching_page.setItem(episode)
        still_watching_page.setProgressStepSize(progress_step_size)
        played_in_a_row_number = utils.settings("playedInARow")
        self.log(
            "played in a row settings %s" % json.dumps(played_in_a_row_number),
            2)
        self.log("played in a row %s" % json.dumps(self.state.played_in_a_row),
                 2)
        showing_next_up_page = False
        showing_still_watching_page = False
        hide_for_short_videos = (
            self.state.short_play_notification
            == "false") and (self.state.short_play_length >= total_time) and (
                self.state.short_play_mode == "true")
        if int(self.state.played_in_a_row) <= int(
                played_in_a_row_number) and not hide_for_short_videos:
            self.log(
                "showing next up page as played in a row is %s" %
                json.dumps(self.state.played_in_a_row), 2)
            next_up_page.show()
            utils.window('service.upnext.dialog', 'true')
            showing_next_up_page = True
        elif not hide_for_short_videos:
            self.log(
                "showing still watching page as played in a row %s" %
                json.dumps(self.state.played_in_a_row), 2)
            still_watching_page.show()
            utils.window('service.upnext.dialog', 'true')
            showing_still_watching_page = True
        while (self.player.isPlaying() and (total_time - play_time > 1)
               and not next_up_page.isCancel()
               and not next_up_page.isWatchNow()
               and not still_watching_page.isStillWatching()
               and not still_watching_page.isCancel()):
            xbmc.sleep(100)
            try:
                play_time = self.player.getTime()
                total_time = self.player.getTotalTime()
                if showing_next_up_page:
                    next_up_page.updateProgressControl()
                elif showing_still_watching_page:
                    still_watching_page.updateProgressControl()
            except Exception as e:
                self.log("error show_popup_and_wait  %s" % repr(e), 1)
                pass
        return showing_next_up_page, showing_still_watching_page, total_time

    def extract_play_info(self, next_up_page, showing_next_up_page,
                          showing_still_watching_page, still_watching_page,
                          total_time):
        if self.state.short_play_length >= total_time and self.state.short_play_mode == "true":
            # play short video and don't add to playcount
            self.state.played_in_a_row += 0
            if next_up_page.isWatchNow(
            ) or still_watching_page.isStillWatching():
                self.state.played_in_a_row = 1
            should_play_default = not next_up_page.isCancel()
        else:
            if showing_next_up_page:
                next_up_page.close()
                utils.window('service.upnext.dialog', clear=True)
                should_play_default = not next_up_page.isCancel()
                should_play_non_default = next_up_page.isWatchNow()
            elif showing_still_watching_page:
                still_watching_page.close()
                utils.window('service.upnext.dialog', clear=True)
                should_play_default = still_watching_page.isStillWatching()
                should_play_non_default = still_watching_page.isStillWatching()

            if next_up_page.isWatchNow(
            ) or still_watching_page.isStillWatching():
                self.state.played_in_a_row = 1
            else:
                self.state.played_in_a_row += 1
        return should_play_default, should_play_non_default
コード例 #8
0
 def __init__(self):
     self.__dict__ = self._shared_state
     self.api = Api()
     self.play_item = PlayItem()
     self.state = State()
     self.player = Player()
コード例 #9
0
class PlaybackManager:
    _shared_state = {}

    def __init__(self):
        self.__dict__ = self._shared_state
        self.api = Api()
        self.play_item = PlayItem()
        self.state = State()
        self.player = Player()

    def log(self, msg, lvl=2):
        class_name = self.__class__.__name__
        utils.log("%s %s" % (utils.addon_name(), class_name), msg, int(lvl))

    def launch_up_next(self):
        episode = self.play_item.get_episode()
        if episode is None:
            # no episode get out of here
            self.log("Error: no episode could be found to play next...exiting", 1)
            return
        self.log("episode details %s" % json.dumps(episode), 2)
        self.launch_popup(episode)
        self.api.reset_addon_data()

    def launch_popup(self, episode):
        episode_id = episode["episodeid"]
        no_play_count = episode["playcount"] is None or episode["playcount"] == 0
        include_play_count = True if self.state.include_watched else no_play_count
        if include_play_count and self.state.current_episode_id != episode_id:
            # we have a next up episode choose mode
            next_up_page, still_watching_page = pages.set_up_pages()
            showing_next_up_page, showing_still_watching_page, total_time = (
                self.show_popup_and_wait(episode, next_up_page, still_watching_page))
            should_play_default, should_play_non_default = (
                self.extract_play_info(next_up_page, showing_next_up_page, showing_still_watching_page,
                                       still_watching_page, total_time))

            play_item_option_1 = (should_play_default and self.state.playMode == "0")
            play_item_option_2 = (should_play_non_default and self.state.playMode == "1")
            if play_item_option_1 or play_item_option_2:
                self.log("playing media episode", 2)
                # Signal to trakt previous episode watched
                utils.event("NEXTUPWATCHEDSIGNAL", {'episodeid': self.state.current_episode_id})
                # Play media
                if not self.api.has_addon_data():
                    self.api.play_kodi_item(episode)
                else:
                    self.api.play_addon_item()

    def show_popup_and_wait(self, episode, next_up_page, still_watching_page):
        play_time = self.player.getTime()
        total_time = self.player.getTotalTime()
        progress_step_size = utils.calculate_progress_steps(total_time - play_time)
        next_up_page.setItem(episode)
        next_up_page.setProgressStepSize(progress_step_size)
        still_watching_page.setItem(episode)
        still_watching_page.setProgressStepSize(progress_step_size)
        played_in_a_row_number = utils.settings("playedInARow")
        self.log("played in a row settings %s" % json.dumps(played_in_a_row_number), 2)
        self.log("played in a row %s" % json.dumps(self.state.played_in_a_row), 2)
        showing_next_up_page = False
        showing_still_watching_page = False
        hide_for_short_videos = (self.state.short_play_notification == "false") and (
                self.state.short_play_length >= total_time) and (
                                        self.state.short_play_mode == "true")
        if int(self.state.played_in_a_row) <= int(played_in_a_row_number) and not hide_for_short_videos:
            self.log(
                "showing next up page as played in a row is %s" % json.dumps(self.state.played_in_a_row), 2)
            next_up_page.show()
            utils.window('service.upnext.dialog', 'true')
            showing_next_up_page = True
        elif not hide_for_short_videos:
            self.log(
                "showing still watching page as played in a row %s" % json.dumps(self.state.played_in_a_row), 2)
            still_watching_page.show()
            utils.window('service.upnext.dialog', 'true')
            showing_still_watching_page = True
        while (self.player.isPlaying() and (
                total_time - play_time > 1) and not next_up_page.isCancel() and not next_up_page.isWatchNow() and
                not still_watching_page.isStillWatching() and not still_watching_page.isCancel()):
            xbmc.sleep(100)
            try:
                play_time = self.player.getTime()
                total_time = self.player.getTotalTime()
                if showing_next_up_page:
                    next_up_page.updateProgressControl()
                elif showing_still_watching_page:
                    still_watching_page.updateProgressControl()
            except Exception as e:
                self.log("error show_popup_and_wait  %s" % repr(e), 1)
                pass
        return showing_next_up_page, showing_still_watching_page, total_time

    def extract_play_info(self, next_up_page, showing_next_up_page, showing_still_watching_page, still_watching_page,
                          total_time):
        if self.state.short_play_length >= total_time and self.state.short_play_mode == "true":
            # play short video and don't add to playcount
            self.state.played_in_a_row += 0
            if next_up_page.isWatchNow() or still_watching_page.isStillWatching():
                self.state.played_in_a_row = 1
            should_play_default = not next_up_page.isCancel()
        else:
            if showing_next_up_page:
                next_up_page.close()
                utils.window('service.upnext.dialog', clear=True)
                should_play_default = not next_up_page.isCancel()
                should_play_non_default = next_up_page.isWatchNow()
            elif showing_still_watching_page:
                still_watching_page.close()
                utils.window('service.upnext.dialog', clear=True)
                should_play_default = still_watching_page.isStillWatching()
                should_play_non_default = still_watching_page.isStillWatching()

            if next_up_page.isWatchNow() or still_watching_page.isStillWatching():
                self.state.played_in_a_row = 1
            else:
                self.state.played_in_a_row += 1
        return should_play_default, should_play_non_default
コード例 #10
0
ファイル: player.py プロジェクト: beam/service.upnext
 def __init__(self):
     self.api = Api()
     self.developer = Developer()
     xbmc.Player.__init__(self)
コード例 #11
0
ファイル: monitor.py プロジェクト: beam/service.upnext
 def __init__(self):
     self.player = Player()
     self.api = Api()
     self.playback_manager = PlaybackManager()
     xbmc.Monitor.__init__(self)
コード例 #12
0
ファイル: plugin.py プロジェクト: jaylinski/kodi-addon-vimeo
from resources.lib.kodi.search_history import SearchHistory
from resources.lib.kodi.settings import Settings
from resources.lib.kodi.utils import format_bold
from resources.lib.kodi.vfs import VFS
from resources.routes import *

addon = xbmcaddon.Addon()
addon_id = addon.getAddonInfo("id")
addon_base = "plugin://" + addon_id
addon_profile_path = xbmcvfs.translatePath(addon.getAddonInfo("profile"))

vfs = VFS(addon_profile_path)
vfs_cache = VFS(os.path.join(addon_profile_path, "cache/"))
settings = Settings(addon)
cache = Cache(settings, vfs_cache)
api = Api(settings, xbmc.getLanguage(xbmc.ISO_639_1), (vfs, vfs_cache), cache)
search_history = SearchHistory(settings, vfs)
listItems = Items(addon, addon_base, settings, search_history, vfs)


def run():
    url = urllib.parse.urlparse(sys.argv[0])
    path = url.path
    handle = int(sys.argv[1])
    args = urllib.parse.parse_qs(sys.argv[2][1:])
    xbmcplugin.setContent(handle, "videos")

    if path == PATH_ROOT:
        action = args.get("action", None)
        if action is None:
            items = listItems.root()
コード例 #13
0
class Monitor(xbmc.Monitor):
    def __init__(self):
        self.player = Player()
        self.api = Api()
        self.playback_manager = PlaybackManager()
        xbmc.Monitor.__init__(self)

    def log(self, msg, lvl=1):
        class_name = self.__class__.__name__
        utils.log("%s %s" % (utils.addon_name(), class_name), str(msg),
                  int(lvl))

    def run(self):

        while not self.abortRequested():
            # check every 1 sec
            if self.waitForAbort(1):
                # Abort was requested while waiting. We should exit
                break
            if self.player.is_tracking():
                try:
                    play_time = self.player.getTime()
                    total_time = self.player.getTotalTime()
                    last_file = self.player.get_last_file()
                    current_file = self.player.getPlayingFile()
                    notification_time = self.api.notification_time()
                    up_next_disabled = utils.settings(
                        "disableNextUp") == "true"
                    if utils.window("PseudoTVRunning"
                                    ) != "True" and not up_next_disabled:
                        if (total_time - play_time <= int(notification_time)
                                and
                            (last_file is None or
                             last_file != current_file)) and total_time != 0:
                            self.player.set_last_file(current_file)
                            self.log(
                                "Calling autoplayback totaltime - playtime is %s"
                                % (total_time - play_time), 2)
                            self.playback_manager.launch_up_next()
                            self.log("Up Next style autoplay succeeded.", 2)
                            self.player.disable_tracking()

                except Exception as e:
                    self.log("Exception in Playback Monitor Service: %s" %
                             repr(e))

                    if 'not playing any media file' in str(e):
                        self.log("No file is playing - stop up next tracking.",
                                 2)
                        self.player.disable_tracking()

        self.log("======== STOP service.upnext ========", 0)

    def onNotification(self, sender, method, data):

        if method.split('.')[1].lower(
        ) != 'upnext_data':  # method looks like Other.upnext_data
            return

        data = utils.decode_data(data)
        data['id'] = "%s_play_action" % str(sender.replace(".SIGNAL", ""))

        self.api.addon_data_received(data)
コード例 #14
0
 def setUp(self):
     self.api = Api(Settings(MagicMock()), "en", MagicMock(), MagicMock())
     self.api.api_cdn = "fastly_skyfire"
     xbmcMock.getUserAgent = Mock(return_value="A User-Agent String")
コード例 #15
0
class ApiTestCase(TestCase):

    def setUp(self):
        self.api = Api(Settings(MagicMock()), "en", MagicMock(), MagicMock())
        self.api.api_cdn = "fastly_skyfire"
        xbmcMock.getUserAgent = Mock(return_value="A User-Agent String")

    def test_search_videos(self):
        with open("./tests/mocks/api_videos_search.json") as f:
            mock_data = f.read()

        self.api._do_api_request = Mock(return_value=json.loads(mock_data))

        self.api.video_av1 = True
        self.api.video_stream = "HLS (Adaptive)"
        res = self.api.search("foo", "bar")

        self.assertEqual(res.items[0].label, "kodi James")
        self.assertEqual(res.items[0].info["user"], "Foo User")
        self.assertEqual(res.items[0].uri, "/videos/13101116")
        self.assertEqual(res.items[0].hasSubtitles, False)
        self.assertEqual(res.items[0].thumb, "https://i.vimeocdn.com/video/74666133_200x150.jpg?r=pad")

        self.assertEqual(res.items[1].label, "Kodi Sings")
        self.assertEqual(res.items[1].info["user"], "Bar User")
        self.assertEqual(res.items[1].uri, "/videos/339780805")
        self.assertEqual(res.items[1].hasSubtitles, False)
        self.assertEqual(res.items[1].thumb, "https://i.vimeocdn.com/video/787910745_200x150.jpg?r=pad")

        # The third item is not playable, so it should not be listed
        self.assertEqual(len(res.items), 2)

    def test_search_videos_no_media_urls(self):
        with open("./tests/mocks/api_videos_search_fallback.json") as f:
            mock_data = f.read()

        self.api._do_api_request = Mock(return_value=json.loads(mock_data))
        self.api.video_stream = "720p"
        res = self.api.search("foo", "videos")

        self.assertEqual(res.items[0].uri, "/videos/13101116")

    def test_search_videos_on_demand(self):
        with open("./tests/mocks/api_videos_search_on_demand.json") as f:
            mock_data = f.read()

        self.api._do_api_request = Mock(return_value=json.loads(mock_data))
        self.api.video_stream = "HLS (Adaptive)"
        res = self.api.search("foo", "videos")

        self.assertEqual(res.items[0].uri, "/videos/372251058")
        self.assertEqual(res.items[0].info["onDemand"], False)

        self.assertEqual(res.items[1].uri, "/ondemand/pages/25096/videos/97663163")
        self.assertEqual(res.items[1].info["onDemand"], True)

        self.assertEqual(res.items[2].uri, "/videos/31158028")
        self.assertEqual(res.items[2].info["onDemand"], False)

    def test_search_videos_live(self):
        with open("./tests/mocks/api_videos_search_live.json") as f:
            mock_data = f.read()

        self.api._do_api_request = Mock(return_value=json.loads(mock_data))
        self.api.video_stream = "HLS (Adaptive)"
        res = self.api.search("foo", "videos")

        self.assertEqual(res.items[0].uri, "/videos/401626792")
        self.assertEqual(res.items[0].info["live"], True)

        self.assertEqual(res.items[1].uri, "/videos/76321431")
        self.assertEqual(res.items[1].info["live"], False)

    def test_search_users(self):
        with open("./tests/mocks/api_users_search.json") as f:
            mock_data = f.read()

        self.api._do_api_request = Mock(return_value=json.loads(mock_data))

        res = self.api.search("foo", "bar")

        self.assertEqual(res.items[0].label, "Petzl-sport")
        self.assertEqual(res.items[0].data["location"], "France")

        self.assertEqual(res.items[1].label, "DSN Digital Sport Network")
        self.assertEqual(res.items[1].data["location"], "")

    def test_search_channels(self):
        with open("./tests/mocks/api_channels_search.json") as f:
            mock_data = f.read()

        self.api._do_api_request = Mock(return_value=json.loads(mock_data))

        res = self.api.search("foo", "bar")

        self.assertEqual(res.items[0].label, "Sport")
        self.assertEqual(res.items[0].thumb, "https://i.vimeocdn.com/video/801804973_640x360.jpg?r=pad")
        self.assertEqual(res.items[0].uri, "/channels/1084121/videos")

        self.assertEqual(res.items[1].label, "Jonica Sport")
        self.assertEqual(res.items[1].thumb, "https://i.vimeocdn.com/video/684400644_640x360.jpg?r=pad")
        self.assertEqual(res.items[1].uri, "/channels/452847/videos")

    def test_search_groups(self):
        with open("./tests/mocks/api_groups_search.json") as f:
            mock_data = f.read()

        self.api._do_api_request = Mock(return_value=json.loads(mock_data))

        res = self.api.search("foo", "bar")

        self.assertEqual(res.items[0].label, "Action Sport - Action Brothers")
        self.assertEqual(res.items[0].thumb, "https://i.vimeocdn.com/video/804634360_640x360.jpg?r=pad")
        self.assertEqual(res.items[0].uri, "/groups/15103/videos")

        self.assertEqual(res.items[1].label, "Sport")
        self.assertEqual(res.items[1].thumb, "https://i.vimeocdn.com/video/804929738_640x360.jpg?r=pad")
        self.assertEqual(res.items[1].uri, "/groups/809/videos")

    def test_channel_videos(self):
        with open("./tests/mocks/api_videos_channel.json") as f:
            mock_data = f.read()

        self.api._do_api_request = Mock(return_value=json.loads(mock_data))
        self.api.video_stream = "720p"

        res = self.api.channel("1")

        self.assertEqual(res.items[0].label, "The Pet Files")
        self.assertEqual(res.items[0].thumb, "https://i.vimeocdn.com/video/857679735_200x150.jpg?r=pad")
        self.assertEqual(res.items[0].uri, "/videos/392544832")

    def test_categories(self):
        with open("./tests/mocks/api_categories.json") as f:
            mock_data = f.read()

        self.api._do_api_request = Mock(return_value=json.loads(mock_data))

        res = self.api.categories()

        self.assertEqual(res.items[0].label, "Animation")
        self.assertEqual(res.items[0].thumb, "https://i.vimeocdn.com/video/858725975_640x360.jpg?r=pad")
        self.assertEqual(res.items[0].uri, "/categories/animation/videos")

        self.assertEqual(res.items[1].label, "Travel")
        self.assertEqual(res.items[1].thumb, "https://i.vimeocdn.com/video/649307891_640x360.jpg?r=pad")
        self.assertEqual(res.items[1].uri, "/categories/travel/videos")

    def test_trending(self):
        with open("./tests/mocks/api_videos_trending.json") as f:
            mock_data = f.read()

        self.api._do_api_request = Mock(return_value=json.loads(mock_data))

        res = self.api.categories()

        self.assertEqual(res.items[0].label, "Feeling Love for Filmfest Dresden")
        self.assertEqual(res.items[1].label, "Lecture: The Meeting with Nadav Kander")
        self.assertEqual(res.items[2].label, "Stay Home")

    def test_resolve_id(self):
        with open("./tests/mocks/api_videos_detail.json") as f:
            mock_data = f.read()

        self.api._do_api_request = Mock(return_value=json.loads(mock_data))

        res = self.api.resolve_id("352494023")

        self.assertEqual(res.items[0].label, "Beautiful Chaos")
        self.assertEqual(res.items[0].thumb, "https://i.vimeocdn.com/video/804395055_200x150.jpg?r=pad")
        self.assertEqual(res.items[0].uri, "/videos/352494023")

        with open("./tests/mocks/api_videos_detail_live.json") as f:
            mock_data = f.read()

        self.api._do_api_request = Mock(return_value=json.loads(mock_data))

        res = self.api.resolve_id("401749070")

        self.assertEqual(res.items[0].label, "Vespers & Benediction: 6PM (CT)")
        self.assertEqual(res.items[0].thumb, "https://i.vimeocdn.com/video/default-live_200x150?r=pad")
        self.assertEqual(res.items[0].uri, "/videos/401749070")

        with open("./tests/mocks/api_videos_detail_unlisted.json") as f:
            mock_data = f.read()

        self.api._do_api_request = Mock(return_value=json.loads(mock_data))

        res = self.api.resolve_id("355062058:5293454954")

        self.assertEqual(res.items[0].label, "An unlisted Vimeo Video")
        self.assertEqual(res.items[0].uri, "/videos/355062058:5293454954")

    def test_resolve_id_password_protected(self):
        with open("./tests/mocks/api_videos_detail_invalid_params.json") as f:
            mock_data = f.read()

        self.api._do_api_request = Mock(return_value=json.loads(mock_data))

        self.assertRaises(PasswordRequiredException, self.api.resolve_id, "216913310")

    def test_resolve_id_texttracks(self):
        with open("./tests/mocks/api_videos_detail_texttracks.json") as f:
            mock_data = f.read()

        self.api._do_api_request = Mock(return_value=json.loads(mock_data))

        res = self.api.resolve_id("140786188")

        self.assertEqual(res.items[0].label, "Impardonnable (English subtitle)")
        self.assertEqual(res.items[0].uri, "/videos/140786188")
        self.assertEqual(res.items[0].hasSubtitles, True)

    def test_resolve_media_url(self):
        with open("./tests/mocks/api_videos_detail.json") as f:
            mock_data = f.read()

        # Progressive
        self.api.video_av1 = False
        self.api.video_stream = "360p"
        self.api._do_api_request = Mock(return_value=json.loads(mock_data))
        res = self.api.resolve_media_url("/videos/352494023")
        self.assertEqual(res, "https://vimeo-prod-skyfire-std-us.storage.googleapis.com/01/498/14/352494023/1430794413.mp4")

        # Progressive (AV1)
        self.api.video_av1 = True
        self.api.video_stream = "1080p"
        self.api._do_api_request = Mock(return_value=json.loads(mock_data))
        res = self.api.resolve_media_url("/videos/352494023")
        self.assertEqual(res, "https://vimeo-prod-skyfire-std-us.storage.googleapis.com/01/498/14/352494023/1446202906.mp4")

        # Progressive (fallback)
        self.api.video_av1 = False
        self.api.video_stream = "720p"  # Resolution does not exist in API response
        self.api._do_api_request = Mock(return_value=json.loads(mock_data))
        res = self.api.resolve_media_url("/videos/352494023")
        self.assertEqual(res, "https://vimeo-prod-skyfire-std-us.storage.googleapis.com/01/498/14/352494023/1430794570.mp4")

        # HLS stream
        self.api.video_stream = "HLS (Adaptive)"
        self.api._do_api_request = Mock(return_value=json.loads(mock_data))
        res = self.api.resolve_media_url("/videos/352494023")
        self.assertEqual(res, "https://player.vimeo.com/play/1446216704/hls")

        with open("./tests/mocks/api_videos_detail_live.json") as f:
            mock_data = f.read()

        # Live stream (HLS)
        self.api.video_stream = "HLS (Adaptive)"
        self.api.video_av1 = True  # Avoids HTTP request in `api._hls_playlist_without_av1_streams`
        self.api._do_api_request = Mock(return_value=json.loads(mock_data))
        res = self.api.resolve_media_url("/videos/401749070")
        self.assertEqual(res, "https://player.vimeo.com/live/7e80cc02-afdd-48fc-9a49-95078c7fbcd3/playlist/hls")

        # Live stream (HLS fallback)
        self.api.video_stream = "720p"
        self.api.video_av1 = True  # Avoids HTTP request in `api._hls_playlist_without_av1_streams`
        self.api._do_api_request = Mock(return_value=json.loads(mock_data))
        res = self.api.resolve_media_url("/videos/401749070")
        self.assertEqual(res, "https://player.vimeo.com/live/7e80cc02-afdd-48fc-9a49-95078c7fbcd3/playlist/hls")

    def test_resolve_media_url_on_demand(self):
        with open("./tests/mocks/api_ondemand_video.json") as f:
            mock_data = f.read()

        self.api._do_api_request = Mock(return_value=json.loads(mock_data))
        self.api.video_stream = "HLS (Adaptive)"

        res = self.api.resolve_media_url("/ondemand/pages/25096")
        self.assertEqual(res, "https://player.vimeo.com/play/260864877/hls")

    def test_resolve_media_url_fallback(self):
        with open("./tests/mocks/player_video_config.json") as f:
            mock_data = f.read()

        self.api.api_fallback = True
        self.api.video_av1 = False

        # Progressive
        self.api.video_stream = "360p"
        self.api._do_player_request = Mock(return_value=json.loads(mock_data))
        res = self.api.resolve_media_url("/videos/13101116")
        self.assertEqual(res, "https://gcs-vimeo.akamaized.net/exp=1570994045~acl=%2A%2F1363060449.mp4%shortened")

        # Progressive (fallback)
        self.api.video_stream = "720p"
        self.api._do_player_request = Mock(return_value=json.loads(mock_data))
        res = self.api.resolve_media_url("/videos/13101116")
        self.assertEqual(res, "https://gcs-vimeo.akamaized.net/exp=1570994045~acl=%2A%2F1363060455.mp4%shortened")

        with open("./tests/mocks/player_video_config_av1.json") as f:
            mock_data = f.read()

        self.api.video_av1 = False

        # HLS stream
        self.api.video_stream = "HLS (Adaptive)"
        self.api._do_player_request = Mock(return_value=json.loads(mock_data))
        res = self.api.resolve_media_url("/videos/13101116")
        self.assertEqual(res, "https://skyfire.vimeocdn.com/avc")

        self.api.video_av1 = True

        # HLS stream (AV1)
        self.api.video_stream = "HLS (Adaptive)"
        self.api._do_player_request = Mock(return_value=json.loads(mock_data))
        res = self.api.resolve_media_url("/videos/13101116")
        self.assertEqual(res, "https://skyfire.vimeocdn.com/av1")

    def test_resolve_media_url_macos(self):
        with open("./tests/mocks/api_videos_detail.json") as f:
            mock_data = f.read()

        xbmcMock.getUserAgent = Mock(return_value="A Mac OS X User Agent")

        self.api.video_stream = "1080p"
        self.api._do_api_request = Mock(return_value=json.loads(mock_data))
        res = self.api.resolve_media_url("/videos/123")
        self.assertEqual(res, "http://a.b/c.mp4|User-Agent=pyvimeo%201.2.0%3B%20%28http%3A//developer.vimeo.com/api/docs%29")

    @skip("Can't easily mock the Vimeo client")
    def test_authorize(self):
        with open("./tests/mocks/api_authorize.json") as f:
            mock_data = json.loads(f.read())

        # self.api.api_client.device_code_authorize = Mock(return_value=(mock_data["access_token"], mock_data["user"], mock_data["scope"]))
        # res = self.api.oauth_device_authorize("72QX6Y", "d1d80ccf128de92517eaac61aad2b539ce2d5af9")
        # self.assertEqual(res, "Philip Gray")

    def test_text_tracks(self):
        with open("./tests/mocks/web.vtt") as f:
            mock_data_webvtt = f.read()

        with open("./tests/mocks/api_videos_texttracks.json") as f:
            mock_data = f.read()

        self.api._do_request = Mock(return_value=mock_data_webvtt)
        self.api._do_api_request = Mock(return_value=json.loads(mock_data))
        res = self.api.resolve_texttracks("/videos/12345/texttracks")

        self.assertEqual(res[0]["uri"], "/videos/503121159/texttracks/11718998")
        self.assertEqual(res[0]["language"], "en-US")
        self.assertTrue(res[0]["srt"].startswith("1\n00:00:08,850 --> 00:00:10,350\nMy love.\n\n"))

        self.assertEqual(res[1]["uri"], "/videos/503121159/texttracks/11719013")
        self.assertEqual(res[1]["language"], "fr")