diff --git a/docs/changelog.rst b/docs/changelog.rst index 7fb81237e..44bfdfd3e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -86,6 +86,8 @@ Other changes: case is shown on separate lines. * Refactored library.py file by splitting it into multiple modules within the beets/library directory. +* Plugin docs slightly reorganized: added a new `all_plugins` page that lists all + plugins in alphabetical order, by default plugins are now categorized 2.3.1 (May 14, 2025) -------------------- diff --git a/docs/conf.py b/docs/conf.py index d0f8cdffe..0102bb151 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -6,6 +6,7 @@ # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information +from docs.pluginlist import PluginListDirective project = "beets" AUTHOR = "Adrian Sampson" @@ -96,3 +97,4 @@ def skip_member(app, what, name, obj, skip, options): def setup(app): app.connect("autodoc-skip-member", skip_member) + app.add_directive("pluginlist", PluginListDirective) diff --git a/docs/pluginlist.py b/docs/pluginlist.py new file mode 100644 index 000000000..45399a8a3 --- /dev/null +++ b/docs/pluginlist.py @@ -0,0 +1,168 @@ +import os +from pathlib import Path + +from docutils import nodes +from docutils.parsers.rst import Directive, directives + + +class PluginListDirective(Directive): + """Directive to list all .rst files in a given folder. + + Along with their top-level header, with optional extra user-defined entries. + + Usage: + .. filelist:: + :path: path/to/folder + :exclude: file1.rst, file2.rst + :extra: + overview.rst # file link with implicit title from filename + Overview: overview.rst # file link with custom title + :ref:`user-guide` # arbitrary reST reference + """ + + has_content = False + required_arguments = 0 + optional_arguments = 0 + + option_spec = { + "path": directives.unchanged_required, + "exclude": directives.unchanged, + "extra": directives.unchanged, + } + + def _extract_title(self, path: Path): + """Extract the first section title from an rst file.""" + with open(path, encoding="utf-8") as f: + lines = [ln.rstrip() for ln in f] + for idx, line in enumerate(lines): + if not line: + continue + # Check for underline-style title + if idx + 1 < len(lines) and set(lines[idx + 1]) <= set( + "= - `:'~^_*+#<>" + ): + underline = lines[idx + 1] + if len(underline) >= len(line): + return line + # Or overline/underline style + if idx >= 1 and set(lines[idx - 1]) <= set("= - `:'~^_*+#<>"): + overline = lines[idx - 1] + if len(overline) >= len(line): + return line + # Fallback: filename without extension + return os.path.splitext(os.path.basename(path))[0] + + def _get_current_src(self) -> Path: + """Get the current source file path.""" + current_doc = self.state.document.current_source + if not current_doc: + raise ValueError("Current document source could not be determined.") + return Path(current_doc).resolve() + + def run(self): + folder_option = self.options.get("path") + if not folder_option: + error = self.state_machine.reporter.error( + 'The "path" option is required for the pluginlist directive.', + nodes.literal_block(self.block_text, self.block_text), + line=self.lineno, + ) + return [error] + + # Resolve folder path relative to current doc file + cur_path = self._get_current_src() + target_folder = cur_path.joinpath(folder_option).resolve().parent + + if not os.path.isdir(target_folder): + error = self.state_machine.reporter.error( + f'Path "{folder_option}" resolved to "{target_folder}". ' + "Could not found or is not a directory.", + nodes.literal_block(self.block_text, self.block_text), + line=self.lineno, + ) + return [error] + + excludes_raw = self.options.get("exclude", "") + excludes = [x.strip() for x in excludes_raw.split(",") if x.strip()] + + # Find .rst files, excluding specified + files = [ + f + for f in os.listdir(target_folder) + if f.endswith(".rst") and f not in excludes + ] + + refs = [] + for filename in files: + # Reference to the rst file + refuri = ( + os.path.splitext(os.path.join(folder_option, filename))[ + 0 + ].replace(os.sep, "/") + + ".html" + ) + # Title for the link + title = self._extract_title(target_folder.joinpath(filename)) + + ref = nodes.reference("", title, internal=True, refuri=refuri) + refs.append(ref) + + # Extra entries into refs + extra_option = self.options.get("extra", "") + if extra_option: + from docutils.statemachine import ViewList + + for line in extra_option.splitlines(): + entry = line.strip() + if not entry: + continue + + para = nodes.paragraph() + + # If entry is pure reST (contains role/backticks and no file .rst) + if ( + "`" in entry or entry.strip().startswith(":ref") + ) and ".rst" not in entry: + vl = ViewList() + vl.append(entry, self.block_text) + self.state.nested_parse(vl, self.content_offset, para) + else: + # file link: either 'file.rst' or 'Title: file.rst' + if ":" in entry: + title, target = [p.strip() for p in entry.split(":", 1)] + else: + target = entry + title = Path(entry).stem + if target.endswith(".rst"): + title = self._extract_title( + target_folder.joinpath(target) + ) + rel = Path(self.options["path"]) / target + refuri = str(rel.with_suffix(".html")).replace( + os.sep, "/" + ) + ref = nodes.reference( + "", title, internal=True, refuri=refuri + ) + para += ref + else: + # fallback parse + vl = ViewList() + vl.append(entry, self.block_text) + self.state.nested_parse(vl, self.content_offset, para) + + refs.append(para) + + # Sort refs + refs.sort(key=lambda x: x.astext().lower()) + + # Build bullet list of links + bullet_list = nodes.bullet_list() + for ref in refs: + item = nodes.list_item() + para = nodes.paragraph() + para += ref + item += para + bullet_list += item + + return [bullet_list] diff --git a/docs/plugins/all_plugins.rst b/docs/plugins/all_plugins.rst new file mode 100644 index 000000000..f70156635 --- /dev/null +++ b/docs/plugins/all_plugins.rst @@ -0,0 +1,60 @@ +All plugins +=========== +.. + README: The plugin list is automatically generated from all plugin + files in the plugins directory. If you want to add an external + plugin, please add it to the extra section of the pluginlist + directive. + + The pluginlist directive is defined in + :file:`docs/pluglist.py` file. + +.. pluginlist:: + :path: . + :exclude: index.rst, all_plugins.rst + :extra: + `beets-yearfixer`_ + `beets-alternatives`_ + `beet-amazon`_ + `beets-artistcountry`_ + `beets-autofix`_ + `beets-autogenre`_ + `beets-audible`_ + `beets-barcode`_ + `beetcamp`_ + `beetstream`_ + `beets-bpmanalyser`_ + `beets-check`_ + `A cmus plugin`_ + `beets-copyartifacts`_ + `beets-describe`_ + `drop2beets`_ + `dsedivec`_ + `beets-filetote`_ + `beets-follow`_ + `beetFs`_ + `beets-goingrunning`_ + `beets-ibroadcast`_ + `beets-id3extract`_ + `beets-importreplace`_ + `beets-jiosaavn`_ + `beets-more`_ + `beets-mosaic`_ + `beets-mpd-utils`_ + `beets-noimport`_ + `beets-originquery`_ + `beets-plexsync`_ + `beets-setlister`_ + `beet-summarize`_ + `beets-usertag`_ + `beets-webm3u`_ + `beets-webrouter`_ + `whatlastgenre`_ + `beets-xtractor`_ + `beets-ydl`_ + `beets-ytimport`_ + `beets-yearfixer`_ + `beets-youtube`_ + +.. include:: index.rst + :start-after: other_links \ No newline at end of file diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 5fbe42d9f..4f4b645d5 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -63,89 +63,47 @@ following to your configuration: source_weight: 0.0 +.. _autotagger_extensions: + + +Available Plugins +----------------- + +We have organized the plugins into several categories to help you find what +you need. The categories are as follows: + +.. contents:: + :local: + :depth: 2 + :backlinks: none + + +If you prefer to browse the plugins by their names, you can make use of the +:doc:`all_plugins ` page, which lists all plugins in alphabetical order. + + .. toctree:: :hidden: - absubmit - acousticbrainz - advancedrewrite - albumtypes - aura - autobpm - badfiles - bareasc - beatport - bpd - bpm - bpsync - bucket - chroma - convert - deezer - discogs - duplicates - edit - embedart - embyupdate - export - fetchart - filefilter - fish - freedesktop - fromfilename - ftintitle - fuzzy - gmusic - hook - ihate - importadded - importfeeds - info - inline - ipfs - keyfinder - kodiupdate - lastgenre - lastimport - limit - listenbrainz - loadext - lyrics - mbcollection - mbsubmit - mbsync - metasync - missing - mpdstats - mpdupdate - musicbrainz - parentwork - permissions - play - playlist - plexupdate - random - replace - replaygain - rewrite - scrub - smartplaylist - sonosupdate - spotify - subsonicplaylist - subsonicupdate - substitute - the - thumbnails - types - unimported - web - zero + all_plugins + -.. _autotagger_extensions: Autotagger Extensions ---------------------- +^^^^^^^^^^^^^^^^^^^^^ + +.. toctree:: + :hidden: + :caption: Autotagger Extensions + :maxdepth: 1 + + chroma + deezer + discogs + fromfilename + musicbrainz + spotify + :doc:`chroma ` Use acoustic fingerprinting to identify audio files with @@ -173,7 +131,31 @@ Autotagger Extensions .. _Spotify: https://www.spotify.com Metadata --------- +^^^^^^^^ + +.. toctree:: + :hidden: + :caption: Metadata + :maxdepth: 1 + + absubmit + acousticbrainz + advancedrewrite + albumtypes + autobpm + bpsync + bpm + edit + embedart + fetchart + ftintitle + keyfinder + lastgenre + lyrics + metasync + replaygain + scrub + zero :doc:`absubmit ` Analyse audio with the `streaming_extractor_music`_ program and submit the metadata to an AcousticBrainz server @@ -247,7 +229,19 @@ Metadata .. _streaming_extractor_music: https://acousticbrainz.org/download Path Formats ------------- +^^^^^^^^^^^^ + +.. toctree:: + :hidden: + :caption: Path Formats + :maxdepth: 1 + + albumtypes + advancedrewrite + inline + rewrite + substitute + the :doc:`albumtypes ` Format album type in path formats. @@ -275,7 +269,21 @@ Path Formats end). Interoperability ----------------- +^^^^^^^^^^^^^^^^ + +.. toctree:: + :hidden: + :caption: Interoperability + :maxdepth: 1 + + aura + embyupdate + kodiupdate + mpdupdate + plexupdate + sonosupdate + subsonicupdate + thumbnails :doc:`aura ` A server implementation of the `AURA`_ specification. @@ -337,7 +345,28 @@ Interoperability .. _Subsonic: http://www.subsonic.org/ Miscellaneous -------------- +^^^^^^^^^^^^^ + +.. toctree:: + :hidden: + :caption: Miscellaneous + :maxdepth: 1 + + bpd + convert + duplicates + filefilter + fuzzy + hook + ihate + info + loadext + mbcollection + mbsubmit + missing + random + types + web :doc:`bareasc ` Search albums and tracks with bare ASCII string matching. @@ -403,10 +432,47 @@ Miscellaneous .. _MPD clients: https://mpd.wikia.com/wiki/Clients .. _mstream: https://github.com/IrosTheBeggar/mStream + +The following plugins are not categorized yet. If you have a strong +opinion about where they should go, please open a `PR or issue `_. + + +.. toctree:: + :caption: Uncategorized Plugins + :maxdepth: 1 + + badfiles.rst + bareasc.rst + beatport.rst + bucket.rst + export.rst + fish.rst + freedesktop.rst + gmusic.rst + importadded.rst + importfeeds.rst + ipfs.rst + lastimport.rst + limit.rst + listenbrainz.rst + mbsync.rst + mpdstats.rst + parentwork.rst + permissions.rst + play.rst + playlist.rst + replace.rst + smartplaylist.rst + subsonicplaylist.rst + unimported.rst + + + + .. _other-plugins: Other Plugins -------------- +^^^^^^^^^^^^^ In addition to the plugins that come with beets, there are several plugins that are maintained by the beets community. To use an external plugin, there @@ -562,6 +628,9 @@ Here are a few of the plugins written by the beets community: `beets-youtube`_ Adds YouTube Music as a tagger data source. +.. + other_links + .. _beets-barcode: https://github.com/8h2a/beets-barcode .. _beetcamp: https://github.com/snejus/beetcamp .. _beetstream: https://github.com/BinaryBrain/Beetstream @@ -609,3 +678,7 @@ Here are a few of the plugins written by the beets community: .. _beets-webm3u: https://github.com/mgoltzsche/beets-webm3u .. _beets-webrouter: https://github.com/mgoltzsche/beets-webrouter .. _beets-autogenre: https://github.com/mgoltzsche/beets-autogenre + + + +