diff --git a/gajim/chat_control.py b/gajim/chat_control.py
index 8841a89be54d33816d600971ef16930b6608fd9d..d8aa5d6e71e7ac9ca161b62e471a048da5c2d424 100644
--- a/gajim/chat_control.py
+++ b/gajim/chat_control.py
@@ -61,6 +61,7 @@ from gajim.gtk.util import get_cursor
 from gajim.gtk.util import ensure_proper_control
 from gajim.gtk.util import format_mood
 from gajim.gtk.util import format_activity
+from gajim.gtk.util import format_tune
 from gajim.gtk.util import get_activity_icon_name
 
 from gajim.command_system.implementation.hosts import ChatCommands
@@ -133,7 +134,6 @@ class ChatControl(ChatControlBase):
         self.update_toolbar()
 
         self._pep_images = {}
-        self._pep_images['tune'] = self.xml.get_object('tune_image')
         self._pep_images['geoloc'] = self.xml.get_object('location_image')
         self.update_all_pep_types()
 
@@ -236,6 +236,8 @@ class ChatControl(ChatControlBase):
             self._on_mood_received)
         app.ged.register_event_handler('activity-received', ged.GUI1,
             self._on_activity_received)
+        app.ged.register_event_handler('tune-received', ged.GUI1,
+            self._on_tune_received)
         if self.TYPE_ID == message_control.TYPE_CHAT:
             # Dont connect this when PrivateChatControl is used
             app.ged.register_event_handler('update-roster-avatar', ged.GUI1,
@@ -420,6 +422,7 @@ class ChatControl(ChatControlBase):
             self.update_pep(pep_type)
         self._update_pep(PEPEventType.MOOD)
         self._update_pep(PEPEventType.ACTIVITY)
+        self._update_pep(PEPEventType.TUNE)
 
     def update_pep(self, pep_type):
         if isinstance(self.contact, GC_Contact):
@@ -460,6 +463,9 @@ class ChatControl(ChatControlBase):
         elif type_ == PEPEventType.ACTIVITY:
             icon = get_activity_icon_name(data.activity, data.subactivity)
             formated_text = format_activity(*data)
+        elif type_ == PEPEventType.TUNE:
+            icon = 'audio-x-generic'
+            formated_text = format_tune(*data)
 
         image.set_from_icon_name(icon, Gtk.IconSize.MENU)
         image.set_tooltip_markup(formated_text)
@@ -470,6 +476,8 @@ class ChatControl(ChatControlBase):
             return self.xml.get_object('mood_image')
         if type_ == PEPEventType.ACTIVITY:
             return self.xml.get_object('activity_image')
+        if type_ == PEPEventType.TUNE:
+            return self.xml.get_object('tune_image')
 
     @ensure_proper_control
     def _on_mood_received(self, _event):
@@ -479,6 +487,10 @@ class ChatControl(ChatControlBase):
     def _on_activity_received(self, _event):
         self._update_pep(PEPEventType.ACTIVITY)
 
+    @ensure_proper_control
+    def _on_tune_received(self, _event):
+        self._update_pep(PEPEventType.TUNE)
+
     @ensure_proper_control
     def _on_nickname_received(self, _event):
         self.update_ui()
@@ -1101,6 +1113,8 @@ class ChatControl(ChatControlBase):
             self._on_mood_received)
         app.ged.remove_event_handler('activity-received', ged.GUI1,
             self._on_activity_received)
+        app.ged.remove_event_handler('tune-received', ged.GUI1,
+            self._on_tune_received)
         if self.TYPE_ID == message_control.TYPE_CHAT:
             app.ged.remove_event_handler('update-roster-avatar', ged.GUI1,
                 self._nec_update_avatar)
diff --git a/gajim/common/modules/user_tune.py b/gajim/common/modules/user_tune.py
index f3e3ea4938e0784ad550b8714c1df5c56f9f682c..9186b8e75991c42b72db34dd779df36edb28a481 100644
--- a/gajim/common/modules/user_tune.py
+++ b/gajim/common/modules/user_tune.py
@@ -15,90 +15,62 @@
 # XEP-0118: User Tune
 
 from typing import Any
-from typing import List  # pylint: disable=unused-import
-from typing import Dict
-from typing import Optional
 from typing import Tuple
 
 import logging
 
 import nbxmpp
-from gi.repository import GLib
 
-from gajim.common.i18n import _
+from gajim.common import app
+from gajim.common.nec import NetworkEvent
+from gajim.common.modules.base import BaseModule
+from gajim.common.modules.util import event_node
+from gajim.common.modules.util import store_publish
 from gajim.common.const import PEPEventType
-from gajim.common.exceptions import StanzaMalformed
-from gajim.common.modules.pep import AbstractPEPModule, AbstractPEPData
-from gajim.common.types import UserTuneDataT
 
 log = logging.getLogger('gajim.c.m.user_tune')
 
 
-class UserTuneData(AbstractPEPData):
-
-    type_ = PEPEventType.TUNE
-
-    def as_markup_text(self) -> str:
-        if self.data is None:
-            return ''
-
-        tune = self.data
-
-        artist = tune.get('artist', _('Unknown Artist'))
-        artist = GLib.markup_escape_text(artist)
-
-        title = tune.get('title', _('Unknown Title'))
-        title = GLib.markup_escape_text(title)
-
-        source = tune.get('source', _('Unknown Source'))
-        source = GLib.markup_escape_text(source)
-
-        tune_string = _('<b>"%(title)s"</b> by <i>%(artist)s</i>\n'
-                        'from <i>%(source)s</i>') % {'title': title,
-                                                     'artist': artist,
-                                                     'source': source}
-        return tune_string
-
-
-class UserTune(AbstractPEPModule):
-
-    name = 'tune'
-    namespace = nbxmpp.NS_TUNE
-    pep_class = UserTuneData
-    store_publish = True
-    _log = log
-
-    def _extract_info(self, item: nbxmpp.Node) -> Optional[Dict[str, str]]:
-        tune_dict = {}
-        tune_tag = item.getTag('tune', namespace=self.namespace)
-        if tune_tag is None:
-            raise StanzaMalformed('No tune node')
-
-        for child in tune_tag.getChildren():
-            name = child.getName().strip()
-            data = child.getData().strip()
-            if child.getName() in ['artist', 'title', 'source',
-                                   'track', 'length']:
-                tune_dict[name] = data
-
-        return tune_dict or None
-
-    def _build_node(self, data: UserTuneDataT) -> nbxmpp.Node:
-        item = nbxmpp.Node('tune', {'xmlns': nbxmpp.NS_TUNE})
-        if data is None:
-            return item
-        artist, title, source, track, length = data
-        if artist:
-            item.addChild('artist', payload=artist)
-        if title:
-            item.addChild('title', payload=title)
-        if source:
-            item.addChild('source', payload=source)
-        if track:
-            item.addChild('track', payload=track)
-        if length:
-            item.addChild('length', payload=length)
-        return item
+class UserTune(BaseModule):
+
+    _nbxmpp_extends = 'Tune'
+    _nbxmpp_methods = [
+        'set_tune',
+    ]
+
+    def __init__(self, con):
+        BaseModule.__init__(self, con)
+        self._register_pubsub_handler(self._tune_received)
+
+    @event_node(nbxmpp.NS_TUNE)
+    def _tune_received(self, _con, _stanza, properties):
+        data = properties.pubsub_event.data
+        empty = properties.pubsub_event.empty
+
+        for contact in app.contacts.get_contacts(self._account,
+                                                 str(properties.jid)):
+            if not empty:
+                contact.pep[PEPEventType.TUNE] = data
+            else:
+                contact.pep.pop(PEPEventType.TUNE, None)
+
+        if properties.is_self_message:
+            if not empty:
+                self._con.pep[PEPEventType.TUNE] = data
+            else:
+                self._con.pep.pop(PEPEventType.TUNE, None)
+
+        app.nec.push_incoming_event(
+            NetworkEvent('tune-received',
+                         account=self._account,
+                         jid=properties.jid.getBare(),
+                         tune=data,
+                         is_self_message=properties.is_self_message))
+
+    @store_publish
+    def set_tune(self, tune):
+        log.info('Send %s', tune)
+        self._nbxmpp('Tune').set_tune(tune)
 
 
 def get_instance(*args: Any, **kwargs: Any) -> Tuple[UserTune, str]:
diff --git a/gajim/common/types.py b/gajim/common/types.py
index ae9cc25987a74843af79424cd8b5efbb096516f2..249e45d7b6339f1fe3e575ab005c197e22cd1e5d 100644
--- a/gajim/common/types.py
+++ b/gajim/common/types.py
@@ -47,8 +47,6 @@ ConnectionT = Union['Connection', 'ConnectionZeroconf']
 ContactsT = Union['Contact', 'GC_Contact']
 ContactT = Union['Contact']
 
-UserTuneDataT = Optional[Tuple[str, str, str, str, str]]
-
 # PEP
 PEPNotifyCallback = Callable[[nbxmpp.JID, nbxmpp.Node], None]
 PEPHandlersDict = Dict[str, List[PEPNotifyCallback]]
diff --git a/gajim/gtk/tooltips.py b/gajim/gtk/tooltips.py
index 74b1d3ac20d3f8cdb8ff23f4124978a32f12b0e1..bc4be71ec7e8922980457be682aec20084bab4be 100644
--- a/gajim/gtk/tooltips.py
+++ b/gajim/gtk/tooltips.py
@@ -44,6 +44,7 @@ from gajim.gtk.util import get_builder
 from gajim.gtk.util import get_icon_name
 from gajim.gtk.util import format_mood
 from gajim.gtk.util import format_activity
+from gajim.gtk.util import format_tune
 
 
 log = logging.getLogger('gajim.gtk.tooltips')
@@ -487,8 +488,8 @@ class RosterTooltip(StatusTable):
             self._ui.activity.show()
             self._ui.activity_label.show()
 
-        if 'tune' in contact.pep:
-            tune = contact.pep['tune'].as_markup_text()
+        if PEPEventType.TUNE in contact.pep:
+            tune = format_tune(*contact.pep[PEPEventType.TUNE])
             self._ui.tune.set_markup(tune)
             self._ui.tune.show()
             self._ui.tune_label.show()
diff --git a/gajim/gtk/util.py b/gajim/gtk/util.py
index 1cd18b868963364cb5961fdf9740f7f9786d9237..9398f1e268a07f1157e55e80935aac8876ddc6d7 100644
--- a/gajim/gtk/util.py
+++ b/gajim/gtk/util.py
@@ -546,3 +546,17 @@ def get_activity_icon_name(activity, subactivity=None):
     if subactivity is not None:
         icon_name += '-%s' % subactivity.replace('_', '-')
     return icon_name
+
+
+def format_tune(artist, length, rating, source, title, track, uri):
+    if artist is None and title is None and source is None:
+        return
+    artist = GLib.markup_escape_text(artist or _('Unknown Artist'))
+    title = GLib.markup_escape_text(title or _('Unknown Title'))
+    source = GLib.markup_escape_text(source or _('Unknown Source'))
+
+    tune_string = _('<b>"%(title)s"</b> by <i>%(artist)s</i>\n'
+                    'from <i>%(source)s</i>') % {'title': title,
+                                                 'artist': artist,
+                                                 'source': source}
+    return tune_string
diff --git a/gajim/gtkgui_helpers.py b/gajim/gtkgui_helpers.py
index a14aaec0402b87b7ea721e1bfee30a609d2d0e42..88587418c7a7790ae08e4a01972e3b42ab5d0cfb 100644
--- a/gajim/gtkgui_helpers.py
+++ b/gajim/gtkgui_helpers.py
@@ -265,9 +265,6 @@ def create_list_multi(value_list, selected_values=None):
     return treeview
 
 def get_pep_icon(pep_class):
-    if pep_class == PEPEventType.TUNE:
-        return 'audio-x-generic'
-
     if pep_class == PEPEventType.LOCATION:
         return 'applications-internet'
 
diff --git a/gajim/gui_interface.py b/gajim/gui_interface.py
index 48dc5d9dc3c1c57a6a972ac18f3cdfdbe2991228..f8eec07c5310af53cb8121ba5e1a77ee40a09474 100644
--- a/gajim/gui_interface.py
+++ b/gajim/gui_interface.py
@@ -47,6 +47,7 @@ from gi.repository import Gio
 from gi.repository import Gdk
 from nbxmpp import idlequeue
 from nbxmpp import Hashes2
+from nbxmpp.structs import TuneData
 import OpenSSL
 
 try:
@@ -1942,8 +1943,8 @@ class Interface:
                 continue
             if app.connections[acct].music_track_info == music_track_info:
                 continue
-            app.connections[acct].get_module('UserTune').send(
-                (artist, title, source, '', ''))
+            app.connections[acct].get_module('UserTune').set_tune(
+                TuneData(artist=artist, title=title, source=source))
             app.connections[acct].music_track_info = music_track_info
 
     def read_sleepy(self):
diff --git a/gajim/roster_window.py b/gajim/roster_window.py
index edc461b8a41be3e04d1208b8c925c2840086ce14..f9d91299011a11307062c20bbbd38d9f95d53d3d 100644
--- a/gajim/roster_window.py
+++ b/gajim/roster_window.py
@@ -1058,9 +1058,8 @@ class RosterWindow:
         else:
             self.model[child_iter][Column.ACTIVITY_PIXBUF] = None
 
-        if app.config.get('show_tunes_in_roster') and 'tune' in pep_dict:
-            self.model[child_iter][Column.TUNE_ICON] = \
-                gtkgui_helpers.get_pep_icon(pep_dict['tune'])
+        if app.config.get('show_tunes_in_roster') and PEPEventType.TUNE in pep_dict:
+            self.model[child_iter][Column.TUNE_ICON] = 'audio-x-generic'
         else:
             self.model[child_iter][Column.TUNE_ICON] = None
 
@@ -1319,7 +1318,7 @@ class RosterWindow:
         if pep_type == PEPEventType.ACTIVITY:
             return app.config.get('show_activity_in_roster')
 
-        if pep_type == 'tune':
+        if pep_type == PEPEventType.TUNE:
             return  app.config.get('show_tunes_in_roster')
 
         if pep_type == 'geoloc':
@@ -1332,6 +1331,7 @@ class RosterWindow:
             self.draw_pep(jid, account, pep_type, contact=contact)
         self._draw_pep(account, jid, PEPEventType.MOOD)
         self._draw_pep(account, jid, PEPEventType.ACTIVITY)
+        self._draw_pep(account, jid, PEPEventType.TUNE)
 
     def draw_pep(self, jid, account, pep_type, contact=None):
         if pep_type not in self._pep_type_to_model_column:
@@ -1373,6 +1373,10 @@ class RosterWindow:
             column = Column.ACTIVITY_PIXBUF
             if data is not None:
                 icon = get_activity_icon_name(data.activity, data.subactivity)
+        elif type_ == PEPEventType.TUNE:
+            column = Column.TUNE_ICON
+            if data is not None:
+                icon = 'audio-x-generic'
 
         for child_iter in iters:
             self.model[child_iter][column] = icon
@@ -2636,8 +2640,7 @@ class RosterWindow:
             self.remove_contact(jid, obj.conn.name, backend=True)
 
     def _nec_pep_received(self, obj):
-        if obj.user_pep.type_ not in (PEPEventType.TUNE,
-                                      PEPEventType.LOCATION):
+        if obj.user_pep.type_ != PEPEventType.LOCATION:
             return
 
         if obj.jid == app.get_jid_from_account(obj.conn.name):
@@ -2655,6 +2658,11 @@ class RosterWindow:
             self.draw_account(event.account)
         self._draw_pep(event.account, event.jid, PEPEventType.ACTIVITY)
 
+    def _on_tune_received(self, event):
+        if event.is_self_message:
+            self.draw_account(event.account)
+        self._draw_pep(event.account, event.jid, PEPEventType.TUNE)
+
     def _on_nickname_received(self, event):
         self.draw_contact(event.jid, event.account)
 
@@ -3595,7 +3603,7 @@ class RosterWindow:
         if active:
             app.interface.enable_music_listener()
         else:
-            app.connections[account].get_module('UserTune').send(None)
+            app.connections[account].get_module('UserTune').set_tune(None)
             # disable music listener only if no other account uses it
             for acc in app.connections:
                 if app.config.get_per('accounts', acc, 'publish_tune'):
@@ -5591,9 +5599,8 @@ class RosterWindow:
         # [icon, name, type, jid, account, editable, mood_pixbuf,
         # activity_pixbuf, TUNE_ICON, LOCATION_ICON, avatar_img,
         # padlock_pixbuf, visible]
-        self.columns = [str, str, str, str, str,
-            str, str, str, str,
-            Gtk.Image, str, bool]
+        self.columns = [str, str, str, str, str, str, str, str, str,
+                        Gtk.Image, str, bool]
 
         self.xml = get_builder('roster_window.ui')
         self.window = self.xml.get_object('roster_window')
@@ -5692,8 +5699,7 @@ class RosterWindow:
         # cell_data_func, func_arg)
         self.renderers_list = []
         self.renderers_propertys = {}
-        self._pep_type_to_model_column = {'tune': Column.TUNE_ICON,
-            'geoloc': Column.LOCATION_ICON}
+        self._pep_type_to_model_column = {'geoloc': Column.LOCATION_ICON}
 
         renderer_text = Gtk.CellRendererText()
         self.renderers_propertys[renderer_text] = ('ellipsize',
@@ -5847,6 +5853,8 @@ class RosterWindow:
             self._on_mood_received)
         app.ged.register_event_handler('activity-received', ged.GUI1,
             self._on_activity_received)
+        app.ged.register_event_handler('tune-received', ged.GUI1,
+            self._on_tune_received)
         app.ged.register_event_handler('update-roster-avatar', ged.GUI1,
             self._nec_update_avatar)
         app.ged.register_event_handler('update-room-avatar', ged.GUI1,