mpdpopm 0.3.5

Maintain ratings & playcounts for your mpd server
Documentation
// Copyright (C) 2020-2025 Michael Herstine <sp1ff@pobox.com>  -*- mode: rust; rust-format-on-save: nil -*-
//
// This file is part of mpdpopm.
//
// mpdpopm is free software: you can redistribute it and/or modify it under the terms of the GNU
// General Public License as published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// mpdpopm is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
// Public License for more details.
//
// You should have received a copy of the GNU General Public License along with mpdpopm.  If not,
// see <http://www.gnu.org/licenses/>.

use lalrpop_util::ParseError;

use crate::filters_ast::{Conjunction, Disjunction, Expression, OpCode, Selector, Term, Value,
                         expect_quoted, parse_iso_8601}; 

grammar;

pub ExprOp: OpCode = {
    "=="       => OpCode::Equality,
    "!="       => OpCode::Inequality,
    "contains" => OpCode::Contains,
    "=~"       => OpCode::RegexMatch,
    "!~"       => OpCode::RegexExclude,
    ">"        => OpCode::GreaterThan,
    "<"        => OpCode::LessThan,
    ">="       => OpCode::GreaterThanEqual,
    "<="       => OpCode::LessThanEqual,
};

pub ExprSel: Selector = {
    r"(?i)artist"                     => Selector::Artist,
    r"(?i)album"                      => Selector::Album,
    r"(?i)albumartist"                => Selector::AlbumArtist,
    r"(?i)titile"                     => Selector::Title,
    r"(?i)track"                      => Selector::Track,
    r"(?i)name"                       => Selector::Name,
    r"(?i)genre"                      => Selector::Genre,
    r"(?i)date"                       => Selector::Date,
    r"(?i)originaldate"               => Selector::OriginalDate,
    r"(?i)composer"                   => Selector::Composer,
    r"(?i)performer"                  => Selector::Performer,
    r"(?i)conductor"                  => Selector::Conductor,
    r"(?i)work"                       => Selector::Work,
    r"(?i)grouping"                   => Selector::Grouping,
    r"(?i)comment"                    => Selector::Comment,
    r"(?i)disc"                       => Selector::Disc,
    r"(?i)label"                      => Selector::Label,
    r"(?i)musicbrainz_aristid"        => Selector::MusicbrainzAristID,
    r"(?i)musicbrainz_albumid"        => Selector::MusicbrainzAlbumID,
    r"(?i)musicbrainz_albumartistid"  => Selector::MusicbrainzAlbumArtistID,
    r"(?i)musicbrainz_trackid"        => Selector::MusicbrainzTrackID,
    r"(?i)musicbrainz_releasetrackid" => Selector::MusicbrainzReleaseTrackID,
    r"(?i)musicbrainz_workid"         => Selector::MusicbrainzWorkID,
    r"(?i)file"                       => Selector::File,
    r"(?i)base"                       => Selector::Base,
    r"(?i)modified-since"             => Selector::ModifiedSince,
    r"(?i)audioformat"                => Selector::AudioFormat,
    r"(?i)rating"                     => Selector::Rating,
    r"(?i)playcount"                  => Selector::PlayCount,
    r"(?i)lastplayed"                 => Selector::LastPlayed,
};

pub Token: Value = {
    <s:r"[0-9]+"> =>? {
        eprintln!("matched token: ``{}''.", s);
        // We need to yield a Result<Value, ParseError>
        match s.parse::<usize>() {
            Ok(n) => Ok(Value::Uint(n)),
            Err(_) => Err(ParseError::User {
                error: "Internal parse error while parsing unsigned int" })
        }
    },
    <s:r#""([ \t'a-zA-Z0-9~!@#$%^&*()-=_+\[\]{}|;:<>,./?]|\\\\|\\"|\\')+""#> => {
        eprintln!("matched token: ``{}''.", s);
        let s = expect_quoted(s).unwrap();
        match parse_iso_8601(&mut s.as_bytes()) {
            Ok(x) => Value::UnixEpoch(x),
            Err(_) => Value::Text(s),
        }
    },
    <s:r#"'([ \t"a-zA-Z0-9~!@#$%^&*()-=_+\[\]{}|;:<>,./?]|\\\\|\\'|\\")+'"#> => {
        eprintln!("matched token: ``{}''.", s);
        let s = expect_quoted(s).unwrap();
        match parse_iso_8601(&mut s.as_bytes()) {
            Ok(x) => Value::UnixEpoch(x),
            Err(_) => Value::Text(s),
        }
    },
};

pub Term: Box<Term> = {
    <t:ExprSel> <u:Token> => {
        eprintln!("matched unary condition: ``({}, {:#?})''", t, u);
        Box::new(Term::UnaryCondition(t, u))
    },
    <t:ExprSel> <o:ExprOp> <u:Token> => {
        eprintln!("matched binary condition: ``({}, {:#?}, {:#?})''", t, o, u);
        Box::new(Term::BinaryCondition(t, o, u))
    },
}

pub Conjunction: Box<Conjunction> = {
    <e1:Expression> "AND" <e2:Expression> => {
        eprintln!("matched conjunction: ``({:#?}, {:#?})''", e1, e2);
        Box::new(Conjunction::Simple(e1, e2))
    },
    <c:Conjunction> "AND" <e:Expression> => {
        eprintln!("matched conjunction: ``({:#?}, {:#?})''", c, e);
        Box::new(Conjunction::Compound(c, e))
    },
}

pub Disjunction: Box<Disjunction> = {
    <e1:Expression> "OR" <e2:Expression> => {
        eprintln!("matched disjunction: ``({:#?}, {:#?})''", e1, e2);
        Box::new(Disjunction::Simple(e1, e2))
    },
    <c:Disjunction> "OR" <e:Expression> => {
        eprintln!("matched disjunction: ``({:#?}, {:#?})''", c, e);
        Box::new(Disjunction::Compound(c, e))
    },
}

pub Expression: Box<Expression> = {
    "(" <t:Term> ")" => {
        eprintln!("matched parenthesized term: ``({:#?})''", t);
        Box::new(Expression::Simple(t))
    },
    "(" "!" <e:Expression> ")" => Box::new(Expression::Negation(e)),
    "(" <c:Conjunction> ")" => {
        eprintln!("matched parenthesized conjunction: ``({:#?})''", c);
        Box::new(Expression::Conjunction(c))
    },
    "(" <c:Disjunction>  ")" => {
        eprintln!("matched parenthesized disjunction: ``({:#?})''", c);
        Box::new(Expression::Disjunction(c))
    },
}