diff --git a/scribbu/Makefile.am b/scribbu/Makefile.am index 97de307..1e4ec2b 100644 --- a/scribbu/Makefile.am +++ b/scribbu/Makefile.am @@ -11,6 +11,7 @@ libscribbu_la_SOURCES = tbt-parser.yy \ framesv22.cc \ framesv23.cc \ framesv24.cc \ + json-pprinter.cc \ id3v2.cc \ id3v22.cc \ id3v23.cc \ @@ -36,6 +37,7 @@ pkginclude_HEADERS = scribbu.hh \ framesv22.hh \ framesv23.hh \ framesv24.hh \ + json-pprinter.hh \ id3v2.hh \ id3v22.hh \ id3v23.hh \ diff --git a/scribbu/json-pprinter.cc b/scribbu/json-pprinter.cc new file mode 100644 index 0000000..0b8ff7b --- /dev/null +++ b/scribbu/json-pprinter.cc @@ -0,0 +1,743 @@ +/** + * \file json-pprinter.cc + * + * Copyright (C) 2025 Michael Herstine + * + * This file is part of scribbu. + * + * scribbu 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. + * + * scribbu 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 scribbu. If not, see . * + * + * + */ + +#include "json-pprinter.hh" + +#include "ostream.hh" +#include "id3v1.hh" +#include "id3v22.hh" +#include "id3v23.hh" +#include "id3v24.hh" + +#include + +#include +#include + +/*static*/ +const boost::optional +scribbu::json_pprinter::DEFAULT_V1ENC = boost::none; + +/*static*/ +const boost::optional +scribbu::json_pprinter::DEFAULT_V2ENC = boost::none; + +/*static*/ +std::string scribbu::json_pprinter::escape(const std::string &s) +{ + std::stringstream stm; + for (char c : s) { + switch (c) { + case '\b': + stm << "\\b"; + break; + case '\f': + stm << "\\f"; + break; + case '\n': + stm << "\\n"; + break; + case '\r': + stm << "\\r"; + break; + case '\t': + stm << "\\t"; + break; + case '"': + case '\\': + stm << '\\' << c; + break; + default: + stm << c; + break; + } + } + return stm.str(); +} + +/*virtual*/ std::ostream & +scribbu::json_pprinter::pprint_v2_2_tag(const id3v2_2_tag &tag, + std::ostream &os) +{ + using namespace std; + + os << R"({"version":"2.2","unsynchronised":)" + << (tag.unsynchronised() ? "true" : "false") + << R"(,"flags":)" << setbase(10) << tag.flags() + << R"(,"size":)" << tag.size() + << R"(,"padding":)" << tag.padding() + << R"(",frames:[")"; + + vector frames; + transform(tag.begin(), tag.end(), back_inserter(frames), + [this](const id3v2_2_frame &frm) { + ostrstream stm; + stm << print_as_json(v1enc_, v2enc_) << frm; + return stm.str(); + }); + + os << boost::algorithm::join(frames, ","); + + return os << "]}"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_v2_3_tag(const id3v2_3_tag &tag, + std::ostream &os) +{ + using namespace std; + + os << R"({"version":"2.3","unsynchronised":)" + << (tag.unsynchronised() ? "true" : "false") + << R"(,"flags":)" << setbase(10) << (int)tag.flags() + << R"(,"size":)" << tag.size() + << R"(,"padding":)" << tag.padding() + << R"(,"frames":[)"; + + vector frames; + transform(tag.begin(), tag.end(), back_inserter(frames), + [this](const id3v2_3_frame &frm) { + ostrstream stm; + stm << print_as_json(v1enc_, v2enc_) << frm << ends; + return stm.str(); + }); + + os << boost::algorithm::join(frames, ","); + + return os << "]}"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_v2_4_tag(const id3v2_4_tag &tag, + std::ostream &os) +{ + using namespace std; + + os << R"({"version":"2.4","unsynchronised":)" + << (tag.unsynchronised() ? "true" : "false") + << R"(,"flags":)" << setbase(10) << tag.flags() + << R"(,"size":)" << tag.size() + << R"(,"experimental":)" << (tag.experimental() ? "true" : "false") + << R"(,"padding":)" << tag.padding() + << R"(",frames:[")"; + + vector frames; + transform(tag.begin(), tag.end(), back_inserter(frames), + [this](const id3v2_4_frame &frm) { + ostrstream stm; + stm << print_as_json(v1enc_, v2enc_) << frm; + return stm.str(); + }); + + os << boost::algorithm::join(frames, ","); + + return os << "]}"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_track_data(const track_data &d, + std::ostream &os) +{ + using namespace std; + + os << R"({"md5":")" << hex << setfill('0'); + vector md5; + d.get_md5(back_inserter(md5)); + + for (auto x: md5) { + os << setw(2) << (unsigned)x; + } + + return os << R"("})"; +} + +/*virtual*/ std::ostream & +scribbu::json_pprinter::pprint_v1_tag(const id3v1_tag &tag, std::ostream &os) +{ + using namespace std; + + encoding dst; + on_no_encoding rsp; + tie(dst, rsp) = encoding_from_stream(os); + + string version("1"); + if (tag.v1_1()) { + version += ".1"; + } + if (tag.enhanced()) { + version += "-enh"; + } + + os << R"({"version":")" << version << R"(","album":")" << + tag.album(v1enc_, dst, rsp) << + R"(","artist":")" << escape(tag.artist(v1enc_, dst, rsp)) << + R"(","comment":")" << escape(tag.comment(v1enc_, dst, rsp)) << + R"(","genre":)" << setbase(10) << (int)tag.genre() << + R"(,"title":")" << escape(tag.title(v1enc_, dst, rsp)) << + R"(","year":")" << escape(tag.year(v1enc_, dst, rsp)) << + R"(")"; + + bool has_track; + unsigned char track; + tie(has_track, track) = tag.track_number(); + if (has_track) { + os << R"(,"track":)" << setbase(10) << (int)track; + } + + if (tag.enhanced()) { + os << R"(,"enhanced-genre":")" << + escape(tag.enh_genre(v1enc_, dst, rsp)) << + R"(")"; + } + + return os << R"(})"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_unk_id3v2_2_frame( + const unknown_id3v2_2_frame &frm, + std::ostream &os) +{ + using namespace std; + + return os << R"({"id":")" << frm.id() << R"(","size":)" + << setbase(10) << (int)frm.size() + << R"(})"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_id3v2_2_text_frame(const id3v2_2_text_frame &frm, + std::ostream &os) +{ + using namespace std; + + encoding dst; + on_no_encoding rsp; + tie(dst, rsp) = encoding_from_stream(os); + + return os << R"({"id":")" << frm.id() << R"(","size":)" + << setbase(10) << (int)frm.size() + << R"(,"text":")" << escape(frm.as_str(dst, rsp, v2enc_)) + << R"("})"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_UFI(const UFI &frm, + std::ostream &os) +{ + using namespace std; + + encoding dst; + on_no_encoding rsp; + tie(dst, rsp) = encoding_from_stream(os); + + return os << R"({"id":")" << frm.id() << R"(","size":)" + << setbase(10) << (int)frm.size() + << R"(,"owner":)" << escape(frm.owner(dst, rsp, v2enc_)) + << R"("})"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_TXX(const TXX &frm, + std::ostream &os) +{ + using namespace std; + + encoding dst; + on_no_encoding rsp; + tie(dst, rsp) = encoding_from_stream(os); + + return os << R"({"id":")" << frm.id() << R"(","size":)" + << setbase(10) << (int)frm.size() + << R"(,"description":")" + << escape(frm.description(dst, rsp, v2enc_)) + << R"(","text":")" + << escape(frm.text(dst, rsp, v2enc_)) + << R"("})"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_COM(const COM &frm, + std::ostream &os) +{ + using namespace std; + + encoding dst; + on_no_encoding rsp; + tie(dst, rsp) = encoding_from_stream(os); + + return os << R"({"id":")" << frm.id() << R"(","size":)" + << setbase(10) << (int)frm.size() + << R"(,"description":")" + << escape(frm.description(dst, rsp, v2enc_)) + << R"(","text":")" + << escape(frm.text(dst, rsp, v2enc_)) + << R"("})"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_CNT(const CNT &cnt, + std::ostream &os) +{ + using namespace std; + + return os << R"({"id":"CNT,"size":)" << setbase(10) << cnt.size() + << R"(,"count":)" << cnt.count() << "}"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_POP(const POP &frm, + std::ostream &os) +{ + using namespace std; + + encoding dst; + on_no_encoding rsp; + tie(dst, rsp) = encoding_from_stream(os); + + encoding src = encoding::UTF_8; + if (v2enc_) { + src = *v2enc_; + } + + return os << R"({"id":")" << frm.id() << R"(","size":)" + << setbase(10) << (int)frm.size() + << R"(,"email":")" + << escape(frm.email(dst, rsp, src)) + << R"(","rating":)" << (unsigned int)frm.rating() + << R"(,"count":)" << frm.count() + << "}"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_XTG(const XTG &frm, + std::ostream &os) +{ + using namespace std; + using boost::algorithm::join; + + vector tags; + transform(frm.begin(), frm.end(), back_inserter(tags), + [](const pair> &pr) { + auto first = escape(pr.first); + switch (pr.second.size()) { + case 0: + return "\"" + first + "\""; + case 1: + return "{\"" + first + R"(":")" + escape(*pr.second.begin()) + + "\"}"; + default: + return "{\"" + first + R"(":[)" + join(pr.second, ",") + "]}"; + } + }); + + return os << R"({"id":")" << frm.id() << R"(",size":)" + << setbase(10) << (int)frm.size() + << R"(,"owner":")" << escape(frm.owner()) + << R"(","cloud":"[)" << join(tags, ",") + << R"(]})"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_unk_id3v2_3_frame( + const unknown_id3v2_3_frame &frm, + std::ostream &os) +{ + using namespace std; + + return os << R"({"id":")" << frm.id() << R"(","size":)" + << setbase(10) << (int)frm.size() + << R"(})"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_id3v2_3_text_frame(const id3v2_3_text_frame &frm, + std::ostream &os) +{ + using namespace std; + + encoding dst; + on_no_encoding rsp; + tie(dst, rsp) = encoding_from_stream(os); + + return os << R"({"id":")" << frm.id() << R"(","size":)" + << setbase(10) << (int)frm.size() + << R"(,"text":")" << escape(frm.as_str(dst, rsp, v2enc_)) + << R"("})"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_UFID(const UFID &frm, + std::ostream &os) +{ + using namespace std; + + encoding dst; + on_no_encoding rsp; + tie(dst, rsp) = encoding_from_stream(os); + + encoding src = encoding::ISO_8859_1; + if (v2enc_) { + src = *v2enc_; + } + + return os << R"({"id":")" << frm.id() << R"(","size":)" + << setbase(10) << (int)frm.size() + << R"(,"owner":)" << escape(frm.owner(dst, rsp, src)) + << R"("})"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_ENCR(const ENCR &frm, + std::ostream &os) +{ + using namespace std; + + encoding dst; + on_no_encoding rsp; + tie(dst, rsp) = encoding_from_stream(os); + + encoding src = encoding::ISO_8859_1; + if (v2enc_) { + src = *v2enc_; + } + + return os << R"({"id":")" << frm.id() << R"(","size":)" << setbase(10) + << (int)frm.size() + << R"(,"email":")" + << escape(frm.email(dst, rsp, src)) + << R"("})"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_TXXX(const TXXX &frm, + std::ostream &os) +{ + using namespace std; + + encoding dst; + on_no_encoding rsp; + tie(dst, rsp) = encoding_from_stream(os); + + return os << R"({"id":")" << frm.id() << R"(","size":)" + << setbase(10) << (int)frm.size() + << R"(,"description":")" + << escape(frm.description(dst, rsp, v2enc_)) + << R"(","text":")" + << escape(frm.text(dst, rsp, v2enc_)) + << R"("})"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_COMM(const COMM &frm, + std::ostream &os) +{ + using namespace std; + + encoding dst; + on_no_encoding rsp; + tie(dst, rsp) = encoding_from_stream(os); + + return os << R"({"id":")" << frm.id() << R"(","size":)" + << setbase(10) << (int)frm.size() + << R"(,"description":")" + << escape(frm.description(dst, rsp, v2enc_)) + << R"(","text":")" + << escape(frm.text(dst, rsp, v2enc_)) + << R"("})"; +} + +/*virtual*/ std::ostream & +scribbu::json_pprinter::pprint_PCNT(const PCNT &pcnt, std::ostream &os) +{ + using namespace std; + + return os << R"({"id":"PCNT,"size":)" << setbase(10) << pcnt.size() + << R"(,"count":)" << pcnt.count() << "}"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_POPM(const POPM &frm, + std::ostream &os) +{ + using namespace std; + + encoding dst; + on_no_encoding rsp; + tie(dst, rsp) = encoding_from_stream(os); + + encoding src = encoding::ISO_8859_1; + if (v2enc_) { + src = *v2enc_; + } + + return os << R"({"id":")" << frm.id() << R"(","size":)" + << setbase(10) << (int)frm.size() + << R"(,"email":")" + << escape(frm.email(dst, rsp, src)) + << R"(","rating":)" << (unsigned int)frm.rating() + << R"(,"count":)" << frm.count() + << "}"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_XTAG(const XTAG &frm, + std::ostream &os) +{ + using namespace std; + using boost::algorithm::join; + + vector tags; + transform(frm.begin(), frm.end(), back_inserter(tags), + [](const pair> &pr) { + auto first = escape(pr.first); + switch (pr.second.size()) { + case 0: + return "\"" + first + "\""; + case 1: + return "{\"" + first + R"(":")" + escape(*pr.second.begin()) + + "\"}"; + default: + return "{\"" + first + R"(":[)" + join(pr.second, ",") + "]}"; + } + }); + + return os << R"({"id":")" << frm.id() << R"(",size":)" + << setbase(10) << (int)frm.size() + << R"(,"owner":")" << escape(frm.owner()) + << R"(","cloud":"[)" << join(tags, ",") + << R"(]})"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_PRIV(const PRIV &frm, + std::ostream &os) +{ + using namespace std; + + encoding dst; + on_no_encoding rsp; + tie(dst, rsp) = encoding_from_stream(os); + + encoding src = encoding::ISO_8859_1; + if (v2enc_) { + src = *v2enc_; + } + + return os << R"({"id":")" << frm.id() << R"(","size":)" << setbase(10) + << (int)frm.size() + << R"(,"email":")" + << escape(frm.email(dst, rsp, src)) + << R"("})"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_unk_id3v2_4_frame( + const unknown_id3v2_4_frame &frm, + std::ostream &os) +{ + using namespace std; + + return os << R"({"id":")" << frm.id() << R"(","size":)" + << setbase(10) << (int)frm.size() + << R"(})"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_id3v2_4_text_frame(const id3v2_4_text_frame &frm, + std::ostream &os) +{ + using namespace std; + + encoding dst; + on_no_encoding rsp; + tie(dst, rsp) = encoding_from_stream(os); + + return os << R"({"id":")" << frm.id() << R"(","size":)" + << setbase(10) << (int)frm.size() + << R"(,"text":")" << escape(frm.as_str(dst, rsp, v2enc_)) + << R"("})"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_UFID_2_4(const UFID_2_4 &frm, std::ostream &os) +{ + using namespace std; + + encoding dst; + on_no_encoding rsp; + tie(dst, rsp) = encoding_from_stream(os); + + encoding src = encoding::ISO_8859_1; + if (v2enc_) { + src = *v2enc_; + } + + return os << R"({"id":")" << frm.id() << R"(","size":)" + << setbase(10) << (int)frm.size() + << R"(,"owner":)" << escape(frm.owner(dst, rsp, src)) + << R"("})"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_ENCR_2_4(const ENCR_2_4 &frm, + std::ostream &os) +{ + using namespace std; + + encoding dst; + on_no_encoding rsp; + tie(dst, rsp) = encoding_from_stream(os); + + encoding src = encoding::ISO_8859_1; + if (v2enc_) { + src = *v2enc_; + } + + return os << R"({"id":")" << frm.id() << R"(","size":)" << setbase(10) + << (int)frm.size() + << R"(,"email":")" + << escape(frm.email(dst, rsp, src)) + << R"("})"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_TXXX_2_4(const TXXX_2_4 &frm, std::ostream &os) +{ + using namespace std; + + encoding dst; + on_no_encoding rsp; + tie(dst, rsp) = encoding_from_stream(os); + + return os << R"({"id":")" << frm.id() << R"(","size":)" + << setbase(10) << (int)frm.size() + << R"(,"description":")" + << escape(frm.description(dst, rsp, v2enc_)) + << R"(","text":")" + << escape(frm.text(dst, rsp, v2enc_)) + << R"("})"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_COMM_2_4(const COMM_2_4 &frm, + std::ostream &os) +{ + using namespace std; + + encoding dst; + on_no_encoding rsp; + tie(dst, rsp) = encoding_from_stream(os); + + return os << R"({"id":")" << frm.id() << R"(","size":)" + << setbase(10) << (int)frm.size() + << R"(,"description":")" + << escape(frm.description(dst, rsp, v2enc_)) + << R"(","text":")" + << escape(frm.text(dst, rsp, v2enc_)) + << R"("})"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_PCNT_2_4(const PCNT_2_4 &pcnt, std::ostream &os) +{ + using namespace std; + + return os << R"({"id":"PCNT,"size":)" << setbase(10) << pcnt.size() + << R"(,"count":)" << pcnt.count() << "}"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_POPM_2_4(const POPM_2_4 &frm, + std::ostream &os) +{ + using namespace std; + + encoding dst; + on_no_encoding rsp; + tie(dst, rsp) = encoding_from_stream(os); + + encoding src = encoding::ISO_8859_1; + if (v2enc_) { + src = *v2enc_; + } + + return os << R"({"id":")" << frm.id() << R"(","size":)" + << setbase(10) << (int)frm.size() + << R"(,"email":")" + << escape(frm.email(dst, rsp, src)) + << R"(","rating":)" << (unsigned int)frm.rating() + << R"(,"count":)" << frm.count() + << "}"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_XTAG_2_4(const XTAG_2_4 &frm, + std::ostream &os) +{ + using namespace std; + using boost::algorithm::join; + + vector tags; + transform(frm.begin(), frm.end(), back_inserter(tags), + [](const pair> &pr) { + auto first = escape(pr.first); + switch (pr.second.size()) { + case 0: + return "\"" + first + "\""; + case 1: + return "{\"" + first + R"(":")" + escape(*pr.second.begin()) + + "\"}"; + default: + return "{\"" + first + R"(":[)" + join(pr.second, ",") + "]}"; + } + }); + + return os << R"({"id":")" << frm.id() << R"(",size":)" + << setbase(10) << (int)frm.size() + << R"(,"owner":")" << escape(frm.owner()) + << R"(","cloud":"[)" << join(tags, ",") + << R"(]})"; +} + +/*virtual*/ std::ostream& +scribbu::json_pprinter::pprint_PRIV_2_4(const PRIV_2_4 &frm, + std::ostream &os) +{ + using namespace std; + + encoding dst; + on_no_encoding rsp; + tie(dst, rsp) = encoding_from_stream(os); + + encoding src = encoding::ISO_8859_1; + if (v2enc_) { + src = *v2enc_; + } + + return os << R"({"id":")" << frm.id() << R"(","size":)" << setbase(10) + << (int)frm.size() + << R"(,"email":")" + << escape(frm.email(dst, rsp, src)) + << R"("})"; +} diff --git a/scribbu/json-pprinter.hh b/scribbu/json-pprinter.hh new file mode 100644 index 0000000..f707fb6 --- /dev/null +++ b/scribbu/json-pprinter.hh @@ -0,0 +1,151 @@ +/** + * \file json-pprinter.hh + * + * Copyright (C) 2025 Michael Herstine + * + * This file is part of scribbu. + * + * scribbu 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. + * + * scribbu 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 scribbu. If not, see . * + * + * + */ + +#ifndef JSON_PPRINTER_INCLUDED_HH +#define JSON_PPRINTER_INCLUDED_HH 1 + +#include + +namespace scribbu { + + /** + * \class json_pprinter + * + * \brief A pretty-printer that produces JSON + * + * I had first thought to implement this using the boost json library, but + * soon realized that wouldn't work with this framework-- I can't accumulate + * the entire object without going through `operator<<`. + * + * + */ + + class json_pprinter : public pprinter { + public: + static const boost::optional DEFAULT_V1ENC; + static const boost::optional DEFAULT_V2ENC; + + public: + static std::string escape(const std::string &s); + + public: + json_pprinter(const boost::optional &v1enc = DEFAULT_V1ENC, + const boost::optional &v2enc = DEFAULT_V2ENC): + v1enc_(v1enc), v2enc_(v2enc) + {} + + public: + virtual std::ostream& + pprint_v2_2_tag(const id3v2_2_tag&, std::ostream&); + virtual std::ostream& + pprint_v2_3_tag(const id3v2_3_tag&, std::ostream&); + virtual std::ostream& + pprint_v2_4_tag(const id3v2_4_tag&, std::ostream&); + virtual std::ostream& + pprint_track_data(const track_data&, std::ostream&); + virtual std::ostream& + pprint_v1_tag(const id3v1_tag&, std::ostream&); + virtual std::ostream& + pprint_unk_id3v2_2_frame(const unknown_id3v2_2_frame &, std::ostream&); + virtual std::ostream& + pprint_id3v2_2_text_frame(const id3v2_2_text_frame&, std::ostream&); + virtual std::ostream& + pprint_UFI(const UFI&, std::ostream&); + virtual std::ostream& + pprint_TXX(const TXX&, std::ostream&); + virtual std::ostream& + pprint_COM(const COM&, std::ostream&); + virtual std::ostream& + pprint_CNT(const CNT&, std::ostream&); + virtual std::ostream& + pprint_POP(const POP&, std::ostream&); + virtual std::ostream& + pprint_XTG(const XTG&, std::ostream&); + virtual std::ostream& + pprint_unk_id3v2_3_frame(const unknown_id3v2_3_frame&, std::ostream&); + virtual std::ostream& + pprint_id3v2_3_text_frame(const id3v2_3_text_frame&, std::ostream&); + virtual std::ostream& + pprint_UFID(const UFID&, std::ostream&); + virtual std::ostream& + pprint_ENCR(const ENCR&, std::ostream&); + virtual std::ostream& + pprint_TXXX(const TXXX&, std::ostream&); + virtual std::ostream& + pprint_COMM(const COMM&, std::ostream&); + virtual std::ostream& + pprint_PCNT(const PCNT&, std::ostream&); + virtual std::ostream& + pprint_POPM(const POPM&, std::ostream&); + virtual std::ostream& + pprint_XTAG(const XTAG&, std::ostream&); + virtual std::ostream& + pprint_PRIV(const PRIV&, std::ostream&); + virtual std::ostream& + pprint_unk_id3v2_4_frame(const unknown_id3v2_4_frame&, std::ostream&); + virtual std::ostream& + pprint_id3v2_4_text_frame(const id3v2_4_text_frame&, std::ostream&); + virtual std::ostream& + pprint_UFID_2_4(const UFID_2_4&, std::ostream&); + virtual std::ostream& + pprint_ENCR_2_4(const ENCR_2_4&, std::ostream&); + virtual std::ostream& + pprint_TXXX_2_4(const TXXX_2_4&, std::ostream&); + virtual std::ostream& + pprint_COMM_2_4(const COMM_2_4&, std::ostream&); + virtual std::ostream& + pprint_PCNT_2_4(const PCNT_2_4&, std::ostream&); + virtual std::ostream& + pprint_POPM_2_4(const POPM_2_4&, std::ostream&); + virtual std::ostream& + pprint_XTAG_2_4(const XTAG_2_4&, std::ostream&); + virtual std::ostream& + pprint_PRIV_2_4(const PRIV_2_4&, std::ostream&); + + virtual ~json_pprinter() + { } + virtual pprinter* clone() { + return new json_pprinter(*this); + } + + private: + boost::optional v1enc_; + boost::optional v2enc_; + }; + + class print_as_json: public pprint_manipulator { + public: + print_as_json(const boost::optional &v1enc = + json_pprinter::DEFAULT_V1ENC, + const boost::optional &v2enc = + json_pprinter::DEFAULT_V2ENC): + pprint_manipulator(new json_pprinter(v1enc, v2enc)) + { } + print_as_json(const json_pprinter &that): + pprint_manipulator(new json_pprinter(that)) + { } + }; + +} // End namespace scribbu. + +#endif // JSON_PPRINTER_INCLUDED_HH diff --git a/scribbu/pprinter.cc b/scribbu/pprinter.cc index f41bd68..ae4f2a7 100644 --- a/scribbu/pprinter.cc +++ b/scribbu/pprinter.cc @@ -1066,10 +1066,8 @@ scribbu::standard_pprinter::print_id3v2_tag(const id3v2_tag &tag, if (tag.has_encoded_by()) { os << sin_ << "Encoded by " << tag.encoded_by(dst, rsp, v2enc_) << "\n"; } - } - std::ostream& scribbu::operator<<(std::ostream &os, const id3v2_tag &tag) { diff --git a/scribbu/pprinter.hh b/scribbu/pprinter.hh index 1acad87..0e1b3c2 100644 --- a/scribbu/pprinter.hh +++ b/scribbu/pprinter.hh @@ -64,9 +64,9 @@ \endcode * * It quickly became clear that this would become a mess; every time I wanted - * to add a new format I would need to remember, and touch every tag & frame - * pretty-print implementation. Each of those functions would become more - * complex as I added more styles of pretty-printing. + * to add a new format I would need to remember, and touch, every tag & frame + * pretty-print implementation. Furthermore, each of those functions would + * become ever more complex as I added more styles of pretty-printing. * * \subsection scribbu_pprinter_discuss_visitor Visitor * @@ -78,7 +78,7 @@ * eliminating the problems described above. The cost would be adding an * "accept" virtual to each tag & frame type, which would then invoke the * relevant visitor method (for two virtual function lookups). There was one - * wrinkle where frames were involved; employing Visitor in this way would mean: + * wrinkle where frames were involved: employing Visitor in this way would mean: * * 1. operator<< constructing a Visitor of the appropriate type and invoking * accept on the tag, passing the Visitor as an argument (one virtual @@ -181,7 +181,7 @@ * 1. it preserves the \ref ref_03 "Open/Closed Principle", unlike my first, * naive implementation (but like both Visitor variants) * - * 2. it breakes up the dependency cycle that Visitor entails + * 2. it breaks up the dependency cycle that Visitor entails * * 3. it captures (and checks at compile-time) the contract a new pretty-print * implementation will have to satisfy (rather than simply writing them diff --git a/src/dump.cc b/src/dump.cc index 0307587..2abfb3a 100644 --- a/src/dump.cc +++ b/src/dump.cc @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -77,7 +78,9 @@ namespace { /// Comma-separated variable format csv, /// Multi-line output format - standard + standard, + /// JavaScript Object Notation + json, }; enum class dump_id3v1 { yes, no }; @@ -92,7 +95,10 @@ namespace { /// Print in CSV format dumper(const boost::regex &file_regex, dump_id3v2 d0, dump_track d1, dump_id3v1 d2, scribbu::encoding v1enc, std::size_t ncomm); - /// Process one directory entitty + /// Print in JSON format + dumper(const boost::regex &file_regex, dump_id3v2 d0, dump_track d1, + dump_id3v1 d2, scribbu::encoding v1enc); + /// Process one directory entity void operator()(const fs::path &pth, bool explict = false); private: @@ -162,6 +168,28 @@ namespace { cout << print_as_csv(ncomm_, v1enc_); } + dumper::dumper(const boost::regex &file_regex, + dump_id3v2 d0, + dump_track d1, + dump_id3v1 d2, + scribbu::encoding v1enc): + file_regex_(file_regex), + fmt_(format::json), + dump_id3v2_(d0 == dump_id3v2::yes), + dump_track_(d1 == dump_track::yes), + dump_id3v1_(d2 == dump_id3v1::yes), + v1enc_(v1enc) + { + using namespace std; + using namespace scribbu; + + if (!dump_id3v2_ && !dump_track_ && !dump_id3v1_) { + dump_id3v2_ = dump_track_ = dump_id3v1_ = true; + } + + cout << print_as_json(v1enc_); + } + /** * \brief Process one directory entitty * @@ -211,7 +239,11 @@ namespace { scribbu::track_data td((istream&)ifs); unique_ptr pid3v1 = scribbu::process_id3v1(ifs); - cout << pth << ":\nLast Modified: " << put_time(localtime(&mtime), TMFMT) << endl; + if (format::json != fmt_) { + cout << pth + << ":\nLast Modified: " << put_time(localtime(&mtime), TMFMT) + << endl; + } if (dump_id3v2_) { for (ptrdiff_t i = 0, n = id3v2.size(); i < n; ++i) { @@ -241,6 +273,7 @@ namespace { { static const boost::regex CSV("1|csv"); static const boost::regex STANDARD("2|standard|std"); + static const boost::regex JSON("3|json"); std::string s; is >> s; @@ -251,6 +284,9 @@ namespace { else if (boost::regex_match(s, STANDARD)) { fmt = dumper::format::standard; } + else if (boost::regex_match(s, JSON)) { + fmt = dumper::format::json; + } else { throw std::invalid_argument("Unknown format"); } @@ -269,6 +305,9 @@ namespace { case dumper::format::standard: os << "standard"; break; + case dumper::format::json: + os << "json"; + break; default: throw std::logic_error("Unknown format"); } @@ -415,13 +454,16 @@ namespace { encoding v1enc = vm["v1-encoding" ].as(); p.reset(new dumper(file_regex, f0, f1, f2, indent, expand, v1enc)); - } - else { + } else if (dumper::format::csv == fmt) { encoding v1enc = vm["v1-encoding" ].as(); size_t ncomm = vm["num-comments"].as(); p.reset(new dumper(file_regex, f0, f1, f2, v1enc, ncomm)); } + else { + encoding v1enc = vm["v1-encoding" ].as(); + p.reset(new dumper(file_regex, f0, f1, f2, v1enc)); + } for (auto x: arguments) { if (fs::is_directory(x)) { diff --git a/test/Makefile.am b/test/Makefile.am index fce50c5..611d189 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -9,7 +9,7 @@ EXTRA_DIST = test-util test-split test-rename test-report test-report-tdf \ test-tagsets-from-scheme.scm test-tagsets-from-scheme \ test-frames-from-scheme.scm test-display.scm test-frames-from-scheme \ test-scripting test-options test-popm test-xtag test-text test-genre \ - test-m3u test-snarfed-in-scribbu test-display + test-m3u test-snarfed-in-scribbu test-display test-json CLEANFILES = id3v20B id3v21B report.last.csv report-tdf.last.tdf \ report.csv report-last.csv report-tdf.tdf \ id3v20A trackB id3v1A trackA \ @@ -32,17 +32,18 @@ CLEANFILES = id3v20B id3v21B report.last.csv report-tdf.last.tdf \ test-m3u-tmp/smoke_tests/LAME_aots_3951.mp3 \ test-m3u-tmp/smoke_tests/sat.mp3 \ test-m3u-tmp/smoke_tests/VBRI_16M.mp3 \ - test-m3u-tmp/smoke_tests/zoe.mp3 + test-m3u-tmp/smoke_tests/zoe.mp3 \ + test-json.out TESTS = $(check_PROGRAMS) test-split test-rename test-report test-report-tdf \ test-dump test-with-track-in test-fs-generator \ test-cleanup-encoded-by test-cleanup-from-audacity \ test-tagsets-from-scheme test-frames-from-scheme test-scripting \ test-options test-popm test-xtag test-text test-genre test-m3u \ - test-snarfed-in-scribbu test-display + test-snarfed-in-scribbu test-display test-json unit_SOURCES = unit.cc unit.hh charsets.cc ostream.cc id3v1.cc framesv2.cc \ framesv22.cc framesv23.cc framesv24.cc id3v2.cc id3v22.cc id3v23.cc \ id3v24.cc id3v2-utils.cc pprinter.cc csv-pprinter.cc mp3.cc tdf-pprinter.cc \ - scribbu.cc id3v2-edit.cc tagset.cc winamp-genres.cc + scribbu.cc id3v2-edit.cc tagset.cc winamp-genres.cc json-pprinter.cc AM_CPPFLAGS = -DBOOST_TEST_DYN_LINK -I$(srcdir)/.. $(BOOST_CPPFLAGS) AM_CXXFLAGS = -std=c++20 diff --git a/test/data/Makefile.am b/test/data/Makefile.am index f171e87..b200e69 100644 --- a/test/data/Makefile.am +++ b/test/data/Makefile.am @@ -11,4 +11,4 @@ duplicate_id3v2.mp3 with-track-in-golden.log unsynch.id3 searchresults.dat \ id3v22-tda.mp3 230-compressed.tag 230-picture.tag ozzy.tag rare_frames.mp3 \ golden-test-cleanup-encoded-by.out id3v2.4.ext.tag exit.tag \ golden-test-cleanup-from-audacity.out v1-only.mp3 wy.mp3 la-mer.mp3 \ -LAME_aots_3951.mp3 VBRI_16M.mp3 sat.mp3 3931.mp3 zoe.mp3 +LAME_aots_3951.mp3 VBRI_16M.mp3 sat.mp3 3931.mp3 zoe.mp3 dump.golden.json diff --git a/test/data/dump.golden.json b/test/data/dump.golden.json new file mode 100644 index 0000000..7e63330 --- /dev/null +++ b/test/data/dump.golden.json @@ -0,0 +1 @@ +{"version":"2.3","unsynchronised":true,"flags":0,"size":295607,"padding":82,"frames":[{"id":"POPM","size":23,"email":"rating@winamp.com","rating":255,"count":0},{"id":"PRIV","size":8207,"email":"www.amazon.com"},{"id":"TIT2","size":67,"text":"Questions Of Travel (LP Version)"},{"id":"TPE1","size":31,"text":"The Ocean Blue"},{"id":"TALB","size":45,"text":"Cerulean (US Release)"},{"id":"TCON","size":35,"text":"Alternative Rock"},{"id":"TPE3","size":1,"text":""},{"id":"TRCK","size":11,"text":"6/12"},{"id":"TYER","size":11,"text":"2005"},{"id":"TPE2","size":31,"text":"The Ocean Blue"},{"id":"COMM","size":68,"description":"","text":"tags=90s,sub-genres=shoegazer"},{"id":"TCOP","size":173,"text":"2005 Warner Bros. Records Inc. Manufactured & Marketed by Warner Strategic Marketing."},{"id":"TPOS","size":9,"text":"1/1"},{"id":"APIC","size":286673}]}{"md5":"d41d8cd98f00b204e9800998ecf8427e"}{"version":"1.1","album":"Cerulean (US Release)","artist":"The Ocean Blue","comment":"tags=90s,sub-genres=shoegaze","genre":255,"title":"Questions Of Travel (LP Versio","year":"2005","track":6} diff --git a/test/json-pprinter.cc b/test/json-pprinter.cc new file mode 100644 index 0000000..6729570 --- /dev/null +++ b/test/json-pprinter.cc @@ -0,0 +1,38 @@ +/** + * \file json-pprinter.cc + * + * Copyright (C) 2025 Michael Herstine + * + * This file is part of scribbu. + * + * scribbu 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. + * + * scribbu 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 scribbu. If not, see . * + * + * + */ + +#include + +#include "unit.hh" + +#include + +BOOST_AUTO_TEST_CASE(test_json_pprinter_escape) +{ + using namespace scribbu; + + BOOST_CHECK("a" == json_pprinter::escape("a")); + BOOST_TEST_MESSAGE(json_pprinter::escape("a b")); + BOOST_CHECK( "a\\tb" == json_pprinter::escape("a b")); + BOOST_CHECK( "the \\\"internet\\\"" == json_pprinter::escape("the \"internet\"")); +} diff --git a/test/test-json b/test/test-json new file mode 100755 index 0000000..dae92e7 --- /dev/null +++ b/test/test-json @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +../src/scribbu dump --format=json ${srcdir}/data/cerulean.mp3 2>&1 > test-json.out +diff test-json.out ${srcdir}/data/dump.golden.json +exit $?