#!/usr/bin/env python __author__ = "Gustavo Sverzut Barbieri" __author_email__ = "barbieri@gmail.com" __license__ = "GPL" __version__ = "0.2" import sys import os import gtk import gobject import hildon import pango import logging import cPickle as Pickle import catota.utils import catota.ui log = logging.getLogger("catota.client") DEF_BIN_PATH = "/usr/bin" class PreferencesProvider(object): preferences_section = None def_preferences = {} def __init__(self): self.gui_preferences = None self.preferences = {} self.preferences.update(self.def_preferences) self.setup_gui_preferences() # __init__() def setup_gui_preferences(self): pass # setup_gui_preferences() def preferences_load(self, d): my_sec = d.get(self.preferences_section, None) if not my_sec: return self.preferences.update(my_sec) # preferences_load() def preferences_save(self, d): my_sec = d.setdefault(self.preferences_section, {}) my_sec.update(self.preferences) # preferences_save() # PreferencesProvider class PlayerEngine(gobject.GObject): name = None priority = 0 log = logging.getLogger("catota.engine") catota.utils.gsignal("state-changed", int) catota.utils.gsignal("error", str) catota.utils.gsignal("eos") catota.utils.gsignal("pos", float) catota.utils.gsignal("media-info", object) STATE_NONE = 0 STATE_PLAYING = 1 STATE_PAUSED = 2 STATE_ERROR = 3 def __init__(self, parent_win, xid=0): gobject.GObject.__init__(self) self.parent_win = parent_win self.xid = xid self.playing = None self.paused = False self.volume = 0 self.mute = False self.pos = 0.0 self.length = None self.info = {} self.fullscreen = False self.state = PlayerEngine.STATE_NONE self.menu_items = None self.setup_menu_items() # __init__() def setup_menu_items(self): pass # setup_menu_items() def play(self, filename): self.playing = filename self.emit("state-changed", PlayerEngine.STATE_PLAYING) return True # play() def pause(self): self.paused = not self.paused self.emit("state-changed", PlayerEngine.STATE_PAUSED) return True # paused() def stop(self): self.playing = None self.emit("state-changed", PlayerEngine.STATE_NONE) return True # stop() SEEK_RELATIVE = 0 SEEK_PERCENT = 1 SEEK_ABSOLUTE = 2 def seek(self, pos, type): return True # seek() def set_volume(self, value): self.volume = value return True # set_volume() def set_mute(self, status): self.mute = status return True # set_mute() def set_fullscreen(self, status): self.fullscreen = status return True # set_fullscreen() def show_osd_message(self, msg, duration): return True # show_osd_message() # PlayerEngine class PlayerSource(object): priority = 0 name = None pretty_name = None log = logging.getLogger("catota.source") def __init__(self, parent_win): self.url = None self.parent_win = parent_win self.options = None self.gui_url_chooser = None self.gui_button_contents = None self.setup_gui_url_chooser() self.setup_gui_button_contents() # __init__() @classmethod def handles(cls, url): return False # handles() def setup_gui_url_chooser(self): pass # setup_gui_url_chooser() def get_url_from_chooser(self): return None # get_url_from_chooser() def setup_gui_button_contents(self): self.gui_button_contents = gtk.Label(self.__class__.__name__) self.gui_button_contents.show() # setup_gui_button_contents() def open(self, url, options=None): self.url = url self.options = options return True # open() def get_player_engine_input_handler(self): return None # get_player_engine_input_handler() def close(self): self.url = None self.options = None return True # close() # PlayerSource class PlayerPreferences(catota.ui.Dialog): def __init__(self, parent): catota.ui.Dialog.__init__(self, "Preferences", parent) self.player = parent self._setup_ui() # __init__() def _setup_ui(self): self.set_has_separator(False) self.tab = gtk.Notebook() self.vbox.pack_start(self.tab, fill=True, expand=True) self._setup_ui_sources() self._setup_ui_advanced() self._setup_ui_player_conf() self.tab.show() # _setup_ui() def _setup_ui_sources(self): wids = [] for o in self.player.sources: if isinstance(o, PreferencesProvider) and o.gui_preferences: wids.extend(o.gui_preferences) self.tab.append_page(catota.ui.new_table(wids), gtk.Label("Sources")) # _setup_ui_sources() def _setup_ui_advanced(self): self.width = hildon.NumberEditor(100, 400) self.height = hildon.NumberEditor(100, 240) wids = ("Media", ("Width:", self.width), ("Height:", self.height), ) self.tab.append_page(catota.ui.new_table(wids), gtk.Label("Advanced")) # _setup_ui_advanced() def _setup_ui_player_conf(self): wids = [] if isinstance(self.player.current_engine, PreferencesProvider): wids.extend(self.player.current_engine.gui_preferences) self.tab.append_page(catota.ui.new_table(wids), gtk.Label("Player")) # _setup_ui_player_conf() def _setup_connections(self): self.connect("response", self._do_response) # _setup_connections() def _fill_data(self): d = self.player.data # Advanced self.width.set_value(d["Media"]["width"]) self.height.set_value(d["Media"]["height"]) if isinstance(self.player.current_engine, PreferencesProvider): self.player.current_engine.preferences_load(d) for o in self.player.sources: if isinstance(o, PreferencesProvider): o.preferences_load(d) # _fill_data() def _save_data(self): d = self.player.data # Advanced d["Media"]["width"] = int(self.width.get_value()) d["Media"]["height"] = int(self.height.get_value()) if isinstance(self.player.current_engine, PreferencesProvider): self.player.current_engine.preferences_save(d) for o in self.player.sources: if isinstance(o, PreferencesProvider): o.preferences_save(d) # _save_data() def run(self, focus_widget=None): self.show() self._fill_data() if not focus_widget: self.tab.set_current_page(0) else: def has_widget(container, wid): for w in container.get_children(): if w == wid: return True elif isinstance(w, gtk.Container): if has_widget(w, wid): return True return False # has_widget() for i in xrange(self.tab.get_n_pages()): w = self.tab.get_nth_page(i) if has_widget(w, focus_widget): self.tab.set_current_page(i) break focus_widget.grab_focus() response = gtk.Dialog.run(self) self.hide() if response == gtk.RESPONSE_ACCEPT: self._save_data() return True else: return False # run() # PlayerPreferences class PlayerUI(hildon.Window): settings_file = os.path.expanduser("~/.catota-player.conf") plugins_source = catota.utils.PluginSet(PlayerSource) plugins_engine = catota.utils.PluginSet(PlayerEngine) def __init__(self, app): hildon.Window.__init__(self) self.app = app app.add_window(self) self.actions = {} self.is_fullscreen = False self.last_played = None self.accel_group = gtk.AccelGroup() self.add_accel_group(self.accel_group) self.action_group = gtk.ActionGroup("PlayerUI") self._setup_actions() self.current_engine = self.plugins_engine[0](self) self.current_source = None self.sources = [] self.current_source = None for cls in self.plugins_source: try: o = cls(self) self.sources.append(o) log.info("loaded source: \"%s\"" % cls.__name__) except Exception, e: log.error("could not instantiate source \"%s\": %s" % (cls.__name__, e)) # BEG: APP Data ## Server self.data = {"Media": {"width": 320, "height": 240, }, "Player": {"volume": 80, "mute": False, } } # END: of APP Data self.media_length = None self.media_title = None self._load_settings() for o in self.sources: if isinstance(o, PreferencesProvider): o.preferences_load(self.data) if isinstance(self.current_engine, PreferencesProvider): self.current_engine.preferences_load(self.data) self._setup_menus() self._setup_toolbar() self._setup_ui() self._setup_connections() # Be maemo-like settings = self.get_settings() settings.set_property("gtk-button-images", False) settings.set_property("gtk-menu-images", False) # Have Task Navigator to blink ;-) #self.set_urgency_hint(True) # __init__() @classmethod def load_plugins_sources(cls, directory): cls.plugins_source.load_from_directory(directory) # load_plugins_sources() @classmethod def load_plugins_engines(cls, directory): cls.plugins_engine.load_from_directory(directory) # load_plugins_engines() def _load_settings(self): if not os.path.exists(self.settings_file): return try: fd = open(self.settings_file) except Exception, e: log.error("Failed to open for reading config file: '%s': %s" % (self.settings_file, e)) return data = Pickle.load(fd) for sec, d in data.iteritems(): n = self.data.setdefault(sec, {}) n.update(d) log.debug("Settings loaded from %s" % self.settings_file) fd.close() # _load_settings() def _save_settings(self): try: fd = open(self.settings_file, "w") except Exception, e: log.error("Failed to open for writing config file: '%s': %s" % (self.settings_file, e)) return for o in self.sources: if isinstance(o, PreferencesProvider): o.preferences_save(self.data) if isinstance(self.current_engine, PreferencesProvider): self.current_engine.preferences_save(self.data) Pickle.dump(self.data, fd, -1) log.debug("Settings saved to %s" % self.settings_file) fd.close() # _save_settings() def _add_stock_action(self, name, label=None, tooltip=None, stock=None): act = gtk.Action(name, label, tooltip, stock) self.action_group.add_action_with_accel(act, None) act.set_accel_group(self.accel_group) act.connect_accelerator() self.actions[name] = act # _add_stock_action() def _setup_actions(self): self._add_stock_action("Quit", stock=gtk.STOCK_QUIT) self._add_stock_action("Preferences", stock=gtk.STOCK_PREFERENCES) self._add_stock_action("Open", stock=gtk.STOCK_OPEN) self._add_stock_action("Play", stock=gtk.STOCK_MEDIA_PLAY) self._add_stock_action("Stop", stock=gtk.STOCK_MEDIA_STOP) act = gtk.ToggleAction("Pause", None, None, gtk.STOCK_MEDIA_PAUSE) self.action_group.add_action_with_accel(act, None) act.set_accel_group(self.accel_group) act.connect_accelerator() self.actions["Pause"] = act # _setup_actions() def _setup_menus(self): menu = gtk.Menu() item = gtk.MenuItem("Player") menu.append(item) item.show() submenu = gtk.Menu() if self.current_engine.menu_items: for wid in self.current_engine.menu_items: submenu.append(wid) item.set_submenu(submenu) submenu.show_all() def add_action(name): item = self.actions[name].create_menu_item() menu.append(item) item.show() item = gtk.MenuItem("Source") menu.append(item) item.show() submenu = gtk.Menu() for o in self.sources: if o.pretty_name: subitem = gtk.MenuItem(o.pretty_name) submenu.append(subitem) subitem.connect("activate", self._source_activated, o) item.set_submenu(submenu) submenu.show_all() add_action("Preferences") item = gtk.SeparatorMenuItem() menu.append(item) item.show() add_action("Quit") self.set_menu(menu) menu.show() # _setup_menus() def _setup_toolbar(self): tb = gtk.Toolbar() tb.set_orientation(gtk.ORIENTATION_HORIZONTAL) tb.set_style(gtk.TOOLBAR_BOTH_HORIZ) self.add_toolbar(tb) self.toolbar = tb self.show_all() # Yep, otherwise toolbar don't show, hildon bug model = gtk.ListStore(str, object) for o in self.sources: if o.pretty_name: model.append((o.pretty_name, o)) ti = gtk.ToolItem() self.source_selector = gtk.ComboBox(model) self.source_selector.connect("changed", self._source_selector_changed) r = gtk.CellRendererText() r.set_property("scale", pango.SCALE_SMALL) r.set_property("scale-set", True) self.source_selector.pack_start(r, True) self.source_selector.add_attribute(r, "text", 0) ti.add(self.source_selector) ti.show_all() tb.insert(ti, -1) ti = self.actions["Open"].create_tool_item() ti.show() tb.insert(ti, -1) self.infotext = catota.ui.Label() ti = gtk.ToolItem() ti.add(self.infotext) ti.set_expand(True) tb.insert(ti, -1) ti.show_all() self.timepos_popup = None self.timepos = gtk.ProgressBar() self.timepos.set_size_request(120, 30) self.timepos.set_text("00:00:00") self.timepos.add_events(gtk.gdk.BUTTON_PRESS_MASK) self.timepos.hide() ti = gtk.ToolItem() ### Work around gtk.ProgressBar() drawing with maemo theme: # maemo theme has a fixed height and starts to redraw the # bar if there is space, so have it to not expand/fill and # add 2 empty spaces before and after in a VBox. box = gtk.VBox() wid = gtk.EventBox() wid.show() box.pack_start(wid, True, True) box.pack_start(self.timepos, False, False) wid = gtk.EventBox() wid.show() box.pack_start(wid, True, True) box.show() ti.add(box) ti.set_expand(False) tb.insert(ti, -1) ti.show() # _setup_toolbar() def _setup_ui(self): self.top_layout = gtk.VBox(homogeneous=False, spacing=0) self.add(self.top_layout) self.pref_dialog = PlayerPreferences(self) self._setup_ui_chooser() self._setup_ui_player() self.chooser_layout.show() self.top_layout.show() self.current_engine.xid = self.video_container.window.xid # _setup_ui() def _setup_ui_chooser(self): self.chooser_layout = gtk.VBox(homogeneous=False, spacing=2) self.top_layout.pack_start(self.chooser_layout, fill=True, expand=True) msg = ("Choose your media source\n" "Click one of the buttons below to be able to " "select what to play") info = catota.ui.MultiLineLabel(msg) info.show() self.chooser_layout.pack_start(info, fill=True, expand=False) box = gtk.VBox(homogeneous=True, spacing=5) box.show() for o in self.sources: btn = gtk.Button() btn.add(o.gui_button_contents) btn.set_size_request(-1, 40) btn.show() btn.connect("clicked", self._source_activated, o) box.pack_start(btn, fill=True, expand=False) sw = gtk.ScrolledWindow() sw.set_shadow_type(gtk.SHADOW_NONE) sw.set_policy(hscrollbar_policy=gtk.POLICY_NEVER, vscrollbar_policy=gtk.POLICY_AUTOMATIC) sw.add_with_viewport(box) sw.show() self.chooser_layout.pack_start(sw, fill=True, expand=True) # _setup_ui_chooser() def _setup_ui_player(self): self.play_layout = gtk.HBox(homogeneous=False, spacing=2) self.top_layout.pack_start(self.play_layout, fill=True, expand=True) vbox = gtk.VBox(homogeneous=True, spacing=5) vbox.set_size_request(80, -1) for name, type in (("Play", gtk.Button), ("Pause", gtk.ToggleButton), ("Stop", gtk.Button)): act = self.actions[name] btn = gtk.Button() btn.set_image(act.create_icon(gtk.ICON_SIZE_DIALOG)) vbox.pack_start(btn, fill=True, expand=False) act.connect_proxy(btn) vbox.show_all() self.play_layout.pack_start(vbox, fill=True, expand=False) self.controls = vbox self.video_container = gtk.EventBox() self.video_container.set_property("can-focus", True) self.video_container.set_events(gtk.gdk.ALL_EVENTS_MASK) self.video_container.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000")) self.play_layout.pack_start(self.video_container, expand=True, fill=True) self.video_container.show() self.volumebar = hildon.VVolumebar() self.volumebar.set_level(self.data["Player"]["volume"]) self.volumebar.set_mute(self.data["Player"]["mute"]) self.play_layout.pack_end(self.volumebar, expand=False, fill=True) self.volumebar.show() # Seek bar self.seekbar = hildon.Seekbar() self.seekbar.set_size_request(600, -1) vbox = gtk.VBox(homogeneous=False, spacing=3) self.seek_title = catota.ui.Label() vbox.pack_start(self.seek_title, fill=True, expand=True) self.cur_time = catota.ui.Label("00:00:00", ellipsize=None) self.total_time = catota.ui.Label("00:00:00", ellipsize=None) hbox = gtk.HBox(homogeneous=False, spacing=3) hbox.pack_start(self.cur_time, fill=True, expand=False) hbox.pack_start(self.seekbar, fill=True, expand=True) hbox.pack_start(self.total_time, fill=True, expand=False) vbox.pack_start(hbox, fill=True, expand=True) w = gtk.Window(gtk.WINDOW_POPUP) w.set_size_request(615, -1) w.set_transient_for(self) w.set_destroy_with_parent(True) w.set_decorated(False) w.set_border_width(6) w.set_gravity(gtk.gdk.GRAVITY_SOUTH_EAST) w.add(vbox) vbox.show_all() width, height = w.get_size() pheight = 58 w.move(gtk.gdk.screen_width() - width, gtk.gdk.screen_height() - height - pheight) self.timepos_popup = w self.timepos_popup_visible = False # _setup_ui() def _setup_connections(self): self.connect("destroy", self.quit) self.connect("window-state-event", self._window_state_changed) self.connect("key-press-event", self._key_pressed) eng_conn = self.current_engine.connect eng_conn("eos", self._player_eos) eng_conn("error", self._player_error) eng_conn("pos", self._player_pos) eng_conn("state-changed", self._player_state_changed) eng_conn("media-info", self._player_media_info) self.timepos_sync_pos_handler = eng_conn("pos", self._seekbar_sync_pos) self.current_engine.handler_block(self.timepos_sync_pos_handler) def action_connect(name, callback): self.actions[name].connect("activate", callback) action_connect("Quit", self.quit) action_connect("Preferences", self._do_preferences) action_connect("Open", self._do_open) action_connect("Play", self._do_play) action_connect("Pause", self._do_pause) action_connect("Stop", self._do_stop) vb_conn = self.volumebar.connect vb_conn("mute_toggled", self._volume_mute_toggle) vb_conn("level_changed", self._volume_changed) self.video_container.connect_after("realize", self._video_container_realize) self.video_container.connect("key-press-event", self._key_pressed) self.seek_autohide = None self.timepos.connect("button-press-event", self._timepos_clicked) self.seekbar_seek_timeout = None self.seekbar_changed_handler = self.seekbar.connect( "value-changed", self._seekbar_changed) # _setup_connections() def _seek_available(self): return self.media_length and self.current_engine and \ self.current_engine.playing # _seek_available() def _seek_update_vals(self): if not self._seek_available(): return self.seek_title.set_label("%s" % self._get_media_title()) self.seekbar.set_position(int(self.current_engine.pos)) self.seekbar.set_total_time(int(self.media_length)) self.total_time.set_markup("%s" % self._time_to_str(self.media_length)) # _seek_update_vals() def _seek_show(self): if not self._seek_available() or self.timepos_popup_visible: return self.timepos_popup_visible = True self._seek_update_vals() width, height = self.timepos_popup.get_size() pheight = 55 self.timepos_popup.move(gtk.gdk.screen_width() - width, gtk.gdk.screen_height() - height - pheight) self.timepos_popup.show() self.current_engine.handler_unblock(self.timepos_sync_pos_handler) if not self.seek_autohide: def f(): self._seek_hide() self.seek_autohide = None return False self.seek_autohide = gobject.timeout_add(5000, f) # _seek_show() def _seek_hide(self): if not self.timepos_popup_visible: return self.timepos_popup_visible = False self.current_engine.handler_block(self.timepos_sync_pos_handler) self.timepos_popup.hide() # _seek_hide() def _timepos_clicked(self, wid, event): if not self._seek_available(): return if not self.timepos_popup_visible: self._seek_show() else: self._seek_hide() # _timepos_clicked() def _do_seek(self): if self.current_engine and self.current_engine.playing: value = self.seekbar.get_value() self.current_engine.seek(value, PlayerEngine.SEEK_ABSOLUTE) # _do_seek() def _seekbar_changed(self, seekbar): if self.seekbar_seek_timeout: return def f(): self._do_seek() self.seekbar_seek_timeout = None return False self.seekbar_seek_timeout = gobject.timeout_add(200, f) # _seekbar_changed() def _seekbar_sync_pos(self, player, pos): self.seekbar.set_position(int(pos)) self.cur_time.set_markup("%s" % self._time_to_str(pos)) # _seekbar_sync_pos() def _volume_changed(self, volumebar): value = int(volumebar.get_level()) self.data["Player"]["volume"] = value self._save_settings() self.current_engine.set_volume(value) # _volume_changed() def _volume_mute_toggle(self, volumebar): state = volumebar.get_mute() self.data["Player"]["mute"] = state self._save_settings() self.current_engine.set_mute(state) # _volume_mute_toggle() def _window_state_changed(self, window, event): state = event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN self.is_fullscreen = bool(state) if self.is_fullscreen: self.controls.hide() self.volumebar.hide() self.toolbar.hide() else: self.controls.show() self.volumebar.show() self.toolbar.show() if self.current_engine.playing: self.current_engine.set_fullscreen(self.is_fullscreen) return False # _window_state_changed() def _source_activated(self, wid, source): self.chooser_layout.hide() self.play_layout.show() idx = self.sources.index(source) self.source_selector.set_active(idx) self.current_source = source # _source_activated() def _source_selector_changed(self, wid): idx = wid.get_active() if idx < 0: return source = wid.get_model()[idx][1] self.current_source = source def f(): self.choose_file() return False gobject.idle_add(f) # _source_selector_changed() def _key_pressed(self, window, event): log.debug("Key pressed %s, event=%s, keyval=%s" % \ (window, event, event.keyval)) if event.keyval == gtk.keysyms.F6: if self.is_fullscreen: self.unfullscreen() else: self.fullscreen() elif event.keyval == gtk.keysyms.F8: level = self.volumebar.get_level() level = max(0, level - 10) self.volumebar.set_level(level) elif event.keyval == gtk.keysyms.F7: level = self.volumebar.get_level() level = min(100, level + 10) self.volumebar.set_level(level) return True # stop others handling it # _key_pressed() def _video_container_realize(self, wid): if not wid.window: return self.current_engine.xid = wid.window.xid # _video_container_realize() def _do_open(self, wid): self.choose_file() # _do_open() def _delayed_play(self, url): if self.current_source.open(url, self.data): self.last_played = url f = self.current_source.get_player_engine_input_handler() d = self.data eng = self.current_engine eng.play(f) eng.set_volume(d["Player"]["volume"]) eng.set_mute(d["Player"]["mute"]) eng.set_fullscreen(self.is_fullscreen) return False # _delayed_play() def _do_play(self, wid): if self.last_played: gobject.idle_add(self._delayed_play, self.last_played) else: self.choose_file() # _do_play() def _do_pause(self, wid): if self.current_engine: self.current_engine.pause() # _do_pause() def _do_stop(self, wid): if self.current_engine: self.current_engine.stop() # _do_stop() def quit(self, *ignored): if self.current_engine.playing: self.current_engine.stop() self._save_settings() gtk.main_quit() # quit() def _player_eos(self, player): self.current_source.close() self.media_length = None # _player_eos() def _player_error(self, player, msg): self.current_engine.stop() self.current_source.close() catota.ui.show_error(self, msg) self.infotext.set_label("Stopped due player error.") self.media_length = None self.timepos.set_text("00:00:00") self.timepos.set_fraction(0) # _player_error() @staticmethod def _time_to_str(time): time = int(time) seconds = time % 60 t = time / 60 minutes = t % 60 hours = t / 60 return "%02d:%02d:%02d" % (hours, minutes, seconds) # _time_to_str() def _player_pos(self, player, pos): elapsed = self._time_to_str(pos) self.timepos.set_text(elapsed) if self.media_length: self.timepos.set_fraction(pos / self.media_length) # _player_pos() def _get_media_title(self): playing = self.current_engine.playing if not playing: return "" if self.media_title: return self.media_title else: if playing.startswith("file://"): fname = playing[len("file://"):] else: fname = playing return os.path.basename(fname) # _get_media_title() def _player_state_changed(self, player, state): if state == PlayerEngine.STATE_NONE: msg = "Idle." self.media_length = None self.timepos.set_text("00:00:00") self.timepos.set_fraction(0) self.timepos.hide() elif state == PlayerEngine.STATE_PLAYING: msg = "Playing %s." % self._get_media_title() self._seek_update_vals() self.timepos.show() elif state == PlayerEngine.STATE_PAUSED: msg = "Paused." self.infotext.set_markup(msg) # _player_state_changed() def _player_media_info(self, player, d): self.media_length = d.get("length", None) self.media_title = d.get("media_title", None) self._seek_update_vals() # _player_media_info() def choose_file(self): if not self.current_source: return url = self.current_source.get_url_from_chooser() if url: gobject.idle_add(self._delayed_play, url) # choose_file() def _do_preferences(self, wid): self.show_preferences() # _do_preferences() def show_preferences(self, focus_widget=None): if self.pref_dialog.run(focus_widget): self._save_settings() # show_preferences() # PlayerUI class PlayerApp(hildon.Program): def __init__(self, name=None): hildon.Program.__init__(self) if name: gtk.set_application_name(name) # __init__() # PlayerApp def load_plugins_sources(directory): PlayerUI.load_plugins_sources(directory) # load_plugins_sources() def load_plugins_engines(directory): PlayerUI.load_plugins_engines(directory) # load_plugins_engines()